<?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: Kehan Hasalawa</title>
    <description>The latest articles on DEV Community by Kehan Hasalawa (@hasalawa).</description>
    <link>https://dev.to/hasalawa</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%2F4155643%2F818d51fc-df02-468a-be3f-89e728650a03.jpg</url>
      <title>DEV Community: Kehan Hasalawa</title>
      <link>https://dev.to/hasalawa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hasalawa"/>
    <language>en</language>
    <item>
      <title>sloth-tidy: a local AI that tidies your messy folder (and lets you undo it)</title>
      <dc:creator>Kehan Hasalawa</dc:creator>
      <pubDate>Sat, 24 Oct 2026 19:52:00 +0000</pubDate>
      <link>https://dev.to/hasalawa/sloth-tidy-a-local-ai-that-tidies-your-messy-folder-and-lets-you-undo-it-10jh</link>
      <guid>https://dev.to/hasalawa/sloth-tidy-a-local-ai-that-tidies-your-messy-folder-and-lets-you-undo-it-10jh</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;My Downloads folder is a museum of good intentions: &lt;code&gt;IMG_0509.png&lt;/code&gt;, &lt;code&gt;final_FINAL_v3.md&lt;/code&gt;, a pile of &lt;code&gt;WhatsApp Image 2026-09-23 at 9.12.19 PM.png&lt;/code&gt;, and an invoice I keep meaning to deal with. Sorting it by hand is exactly the kind of chore I put off forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sloth-tidy&lt;/strong&gt; is a lazy, local, undoable file tidier. You point it at a folder. A small open-weight model running on my own laptop reads each file's name and the start of its contents, decides what the file &lt;em&gt;is&lt;/em&gt;, gives it a clean name, and files it away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receipts/    invoice-7qecmbeg-0001.pdf, payment-receipt-0612-9321.pdf
Documents/   daily-notes.docx
Images/      whatsapp-image-2026-09-23.png, ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The only effort left for me is one click to approve. It has a small browser UI (&lt;code&gt;python3 sloth_tidy.py gui&lt;/code&gt;) and a CLI, including a &lt;code&gt;watch&lt;/code&gt; mode that tidies a folder in the background every few minutes.&lt;/p&gt;

&lt;p&gt;Because a lazy tool must not be a reckless one, it is built to be safe to be lazy with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dry run first.&lt;/strong&gt; It shows the plan, grouped by category, and nothing moves until I say so. I can untick anything I disagree with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo.&lt;/strong&gt; Every run is logged, and one click puts every file back where it was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never overwrites.&lt;/strong&gt; Name collisions get &lt;code&gt;-1&lt;/code&gt;, &lt;code&gt;-2&lt;/code&gt; suffixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is careful with a real Downloads folder.&lt;/strong&gt; A file that is open in another program is skipped and reported (everything else still moves and stays undoable), and unfinished downloads (&lt;code&gt;.crdownload&lt;/code&gt;, &lt;code&gt;.part&lt;/code&gt;) and system files like &lt;code&gt;desktop.ini&lt;/code&gt; are left alone.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_PKF_330Mns" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The video shows the whole flow on a real folder of 13 files: a no-AI dry run, the real run with the model, reviewing the plan, moving the files, and Undo.&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/Hasalawa" rel="noopener noreferrer"&gt;
        Hasalawa
      &lt;/a&gt; / &lt;a href="https://github.com/Hasalawa/sloth-tidy" rel="noopener noreferrer"&gt;
        sloth-tidy
      &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;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/Hasalawa/sloth-tidy/assets/hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHasalawa%2Fsloth-tidy%2FHEAD%2Fassets%2Fhero.png" alt="sloth-tidy: a local AI tidies your messy folder, then you can undo it" width="100%"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;
  &lt;a href="https://github.com/Hasalawa/sloth-tidy/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/Hasalawa/sloth-tidy/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/80d8d8af89902cf664e9e0be9b16cd3939ecdb7fe9d1e7a8afd03d00423e89a2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e392532422d3337373641423f6c6f676f3d707974686f6e266c6f676f436f6c6f723d7768697465"&gt;&lt;img src="https://camo.githubusercontent.com/80d8d8af89902cf664e9e0be9b16cd3939ecdb7fe9d1e7a8afd03d00423e89a2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e392532422d3337373641423f6c6f676f3d707974686f6e266c6f676f436f6c6f723d7768697465" alt="Python 3.9+"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/acd7cd826a2f24be03bdc800e5db16145f8e5d04ffd101ad927c7f125fad86d7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646570656e64656e636965732d6e6f6e652d386662333765"&gt;&lt;img src="https://camo.githubusercontent.com/acd7cd826a2f24be03bdc800e5db16145f8e5d04ffd101ad927c7f125fad86d7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646570656e64656e636965732d6e6f6e652d386662333765" alt="No dependencies"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/2723caceb6db0c8756c4c92c1fc1168eb778272c22f6e816f78d1e3ece87b583/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f72756e732d3130302532352532306c6f63616c6c792d386662333765"&gt;&lt;img src="https://camo.githubusercontent.com/2723caceb6db0c8756c4c92c1fc1168eb778272c22f6e816f78d1e3ece87b583/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f72756e732d3130302532352532306c6f63616c6c792d386662333765" alt="Runs 100% locally"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/cbe493e083f0a848dc1f82c0f953cc4ec03ee23dd219b5acf1df53f38c2f5b1a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f41492d4f6c6c616d612532302543322542372532306f70656e2d2d7765696768742d626c61636b3f6c6f676f3d6f6c6c616d61"&gt;&lt;img src="https://camo.githubusercontent.com/cbe493e083f0a848dc1f82c0f953cc4ec03ee23dd219b5acf1df53f38c2f5b1a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f41492d4f6c6c616d612532302543322542372532306f70656e2d2d7765696768742d626c61636b3f6c6f676f3d6f6c6c616d61" alt="Ollama, open-weight models"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/Hasalawa/sloth-tidy/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b8cadaa967891081f8f165695470689986c028821dd8a040132f6e661795dc0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c7565" alt="MIT license"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;A lazy, local, undoable file tidier.&lt;/h3&gt;
&lt;/div&gt;

&lt;p&gt;
  A small open-weight model running in &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt; reads each file's name and a short snippet,&lt;br&gt;
  picks a category and a clean file name, and moves it. &lt;b&gt;Nothing leaves your machine.&lt;/b&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://youtu.be/_PKF_330Mns" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHasalawa%2Fsloth-tidy%2FHEAD%2Fassets%2Fdemo-thumbnail.png" alt="Watch the 2-minute demo on YouTube" width="620"&gt;
  &lt;/a&gt;&lt;br&gt;
  ▶ Watch the 2-minute demo
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built for the &lt;strong&gt;DEV Hacktoberfest Open-Source AI Challenge, Week 3: &lt;em&gt;Slow and Steady&lt;/em&gt;&lt;/strong&gt;
Started and finished inside the challenge window (Oct 19 to Oct 25, 2026).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;✨ Why you might like it&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;


&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;🧠 &lt;strong&gt;Reads, not just sorts&lt;/strong&gt;&lt;br&gt;
&lt;/td&gt;

&lt;td&gt;A rule engine only sees the extension. The model also sees the file's name and, for text files, its first lines, so an invoice can land in &lt;code&gt;Receipts/&lt;/code&gt; with a readable name.&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;🔒 &lt;strong&gt;Private by construction&lt;/strong&gt;&lt;br&gt;
&lt;/td&gt;

&lt;td&gt;Files are read by a model on your own machine. No API key, no upload, works offline.&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;👀 &lt;strong&gt;Look before it moves&lt;/strong&gt;&lt;br&gt;
&lt;/td&gt;

&lt;td&gt;Dry run by default. You get a plan grouped by category and can&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;…&lt;p&gt;&lt;/p&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Hasalawa/sloth-tidy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;One Python file, standard library only, plus tests and GitHub Actions CI. No &lt;code&gt;pip install&lt;/code&gt;, no API key, no account.&lt;/p&gt;

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

&lt;p&gt;The open pieces that make it work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; for local inference, with &lt;strong&gt;&lt;code&gt;llama3.2:3b&lt;/code&gt;&lt;/strong&gt;, an open-weight model, as the default. The &lt;code&gt;--model&lt;/code&gt; flag (or the dropdown in the UI) swaps in any model you have pulled.&lt;/li&gt;
&lt;li&gt;Ollama's &lt;strong&gt;structured outputs&lt;/strong&gt;: the request carries a JSON schema where &lt;code&gt;category&lt;/code&gt; is an &lt;code&gt;enum&lt;/code&gt;, so the model has to pick from my list and return &lt;code&gt;{category, new_name}&lt;/code&gt;. That is what makes a 3B model reliable enough for this job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline is deliberately boring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For each file, send the name, size and the first 600 characters (text-like files only) to Ollama's &lt;code&gt;/api/chat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Treat the model as an &lt;strong&gt;untrusted advisor&lt;/strong&gt;. The suggested name is slugified (a model answering &lt;code&gt;../../evil&lt;/code&gt; produces a file called &lt;code&gt;evil&lt;/code&gt;), unknown categories are thrown away, and the browser UI's server re-validates every move it is asked to make.&lt;/li&gt;
&lt;li&gt;If Ollama is down, or the model returns junk for one file, that file falls back to simple extension rules. The tool degrades instead of breaking.&lt;/li&gt;
&lt;li&gt;Every real run is written to a log inside the folder, which is what makes Undo possible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I tested the logic and the local web UI's API against a fake Ollama server (20 tests, including path traversal, name collisions, garbage model output, Ollama being offline, locked files, unfinished downloads, Windows reserved names and tampered requests), so CI needs no GPU and no model. I also drove the UI in a real browser end to end.&lt;/p&gt;

&lt;p&gt;I built this with the help of an AI assistant (Claude) for coding, testing and the demo editing, which the challenge allows.&lt;/p&gt;

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

&lt;p&gt;A file tidier is about the most privacy-sensitive thing you can point an AI at. It reads receipts, scans, notes and screenshots. With a closed API, "tidy my Downloads" means uploading my Downloads. With a local open-weight model, the path is: disk, my own RAM, disk. The files never leave the machine, and it works with the network unplugged.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by construction&lt;/strong&gt;, not by policy. There is no server I have to trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-file cost.&lt;/strong&gt; A cleanup touches many files, and a &lt;code&gt;watch&lt;/code&gt; loop touches them again and again. Per-token pricing punishes exactly the lazy, repetitive automation this week's theme asks for. Locally, the cost is electricity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I control the behaviour.&lt;/strong&gt; The categories and the prompt are a few lines at the top of one file, and &lt;code&gt;--model&lt;/code&gt; swaps the model. A bigger model for a big cleanup, a tiny one for a nightly &lt;code&gt;watch&lt;/code&gt;. Nobody can deprecate or change it under me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output is a feature of the runtime I control&lt;/strong&gt;, and it is what made a small model dependable enough to ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where open did not win:&lt;/strong&gt; being honest about the trade-off. The 3B model named my camera photos fairly generically (&lt;code&gt;image-from-snippet.png&lt;/code&gt;, &lt;code&gt;image-from-screenshot.png&lt;/code&gt;) because it only sees a filename and no pixels, and it took roughly a minute on my laptop's CPU for 13 files. A large hosted model would probably name things better and faster. For this job I accept that: the review step catches the weak names, and nothing private is uploaded.&lt;/p&gt;

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

&lt;p&gt;On a real folder from my Downloads (13 files: a document, 10 images, an invoice and a receipt):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The no-AI dry run was instant. The model run took about a minute on my laptop CPU, and I did something else while it ran.&lt;/li&gt;
&lt;li&gt;The model put the invoice and the receipt in &lt;code&gt;Receipts/&lt;/code&gt; with clean names. The 10 images went to &lt;code&gt;Images/&lt;/code&gt; with names that were readable but not always descriptive.&lt;/li&gt;
&lt;li&gt;Total effort from me: paste a path, click &lt;strong&gt;Take a look&lt;/strong&gt;, click &lt;strong&gt;Move&lt;/strong&gt;. Undo restored all 13 files exactly, both times I tried it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have not measured how much time this saves over weeks, so I will not pretend to. The point is that the chore now costs me two clicks instead of an afternoon I would never schedule.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ollama</category>
      <category>python</category>
    </item>
    <item>
      <title>ReceiptSorter: receipts in, expense sheet out, with open models on your own laptop</title>
      <dc:creator>Kehan Hasalawa</dc:creator>
      <pubDate>Wed, 14 Oct 2026 11:12:43 +0000</pubDate>
      <link>https://dev.to/hasalawa/receiptsorter-receipts-in-expense-sheet-out-with-open-models-on-your-own-laptop-ono</link>
      <guid>https://dev.to/hasalawa/receiptsorter-receipts-in-expense-sheet-out-with-open-models-on-your-own-laptop-ono</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;ReceiptSorter&lt;/strong&gt; turns a pile of receipts and invoices into a clean expense CSV. You drop in text files, PDFs or &lt;strong&gt;phone photos of receipts&lt;/strong&gt;, and open-weight models running on your own computer read each one and pull out the vendor, date, total, currency and an expense category. You fix anything that looks off in an editable table, then download a CSV or a ZIP with the receipts renamed and sorted into category folders. A receipt-style tape on the side adds up the totals by category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for it:&lt;/strong&gt; freelancers and one-person businesses who dread month-end bookkeeping, and the small accountants and bookkeepers who receive a messy bundle of receipt photos from clients every month. The invoices they handle name their clients and show what they charge, which is exactly the data many of them do not want to upload to a random cloud service. That makes "nothing leaves your laptop" a real selling point rather than a footnote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem it solves:&lt;/strong&gt; every month-end they lose hours renaming files, retyping totals into a spreadsheet and guessing categories. ReceiptSorter turns that into drag, check, download.&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%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyahqxc7g1c7yarxd91bx.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%2Fyahqxc7g1c7yarxd91bx.png" alt="ReceiptSorter web UI with sorted receipts and the totals tape" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run it yourself in under a minute (the sample receipts need no model at all):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Hasalawa/receiptsorter
&lt;span class="nb"&gt;cd &lt;/span&gt;receiptsorter
python3 webapp.py        &lt;span class="c"&gt;# opens http://127.0.0.1:8765, then click "Try sample receipts"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For the AI part, install &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; and pull the models once:&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 qwen2.5:7b-instruct   &lt;span class="c"&gt;# text and PDF receipts&lt;/span&gt;
ollama pull qwen2.5vl:7b          &lt;span class="c"&gt;# photos of receipts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is also a CLI for folders of receipts:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 receiptsorter.py samples --out sorted --move-files
- cafe.txt: Cafe Ceylon | 2026-10-08 | 4100.0 LKR | Meals
- hosting.txt: Lanka Cloud Hosting (Pvt) Ltd | 2026-09-03 | 8500.0 LKR | Software
- keyboard.txt: Tech Mart Electronics | 2026-10-01 | 24990.0 LKR | Hardware
- taxi.txt: PickMe Rides | 2026-09-12 | 3250.0 LKR | Travel

Wrote sorted/receipts.csv (4 receipts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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/Hasalawa" rel="noopener noreferrer"&gt;
        Hasalawa
      &lt;/a&gt; / &lt;a href="https://github.com/Hasalawa/receiptsorter" rel="noopener noreferrer"&gt;
        receiptsorter
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Local-first receipt &amp;amp; invoice sorter for freelancers. Drop in text, PDF or phone photos; open-weight models (via Ollama) extract vendor, date, total and category into a CSV, so client data never leaves your laptop.
    &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;ReceiptSorter&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/123665043/673295490-75c2229f-f540-463f-9ecc-c0bcc865abd0.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTE5NzY2NjMsIm5iZiI6MTc5MTk3NjM2MywicGF0aCI6Ii8xMjM2NjUwNDMvNjczMjk1NDkwLTc1YzIyMjlmLWY1NDAtNDYzZi05ZWNjLWMwYmNjODY1YWJkMC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYxMDE0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MTAxNFQxMTEyNDNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xNGM2N2U1MWU3NTkwM2Q1NDE3OWIyNGI0Njc1OTk4ZTg1M2I5ODc0MzA2OGYyNGYyZjhhZWNmMjRkMGUxNTUyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZyZXNwb25zZS1jb250ZW50LXR5cGU9aW1hZ2UlMkZwbmcifQ.Ni6SePErKOfQt4cJ_IbWxPoILERoY9-ujGVB1Z3UR48"&gt;&lt;img width="2000" height="840" alt="cover" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F123665043%2F673295490-75c2229f-f540-463f-9ecc-c0bcc865abd0.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTE5NzY2NjMsIm5iZiI6MTc5MTk3NjM2MywicGF0aCI6Ii8xMjM2NjUwNDMvNjczMjk1NDkwLTc1YzIyMjlmLWY1NDAtNDYzZi05ZWNjLWMwYmNjODY1YWJkMC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYxMDE0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MTAxNFQxMTEyNDNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xNGM2N2U1MWU3NTkwM2Q1NDE3OWIyNGI0Njc1OTk4ZTg1M2I5ODc0MzA2OGYyNGYyZjhhZWNmMjRkMGUxNTUyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZyZXNwb25zZS1jb250ZW50LXR5cGU9aW1hZ2UlMkZwbmcifQ.Ni6SePErKOfQt4cJ_IbWxPoILERoY9-ujGVB1Z3UR48" class="js-gh-image-fallback"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sort freelancer receipts and invoices into a clean CSV and category folders, &lt;strong&gt;entirely on your own machine&lt;/strong&gt;
A local open-weight model (via &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;) reads each receipt and extracts vendor, date, total, currency and expense category. No cloud API, no client data leaving your laptop.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Web app (easiest)&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python3 webapp.py&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Opens &lt;code&gt;http://127.0.0.1:8765&lt;/code&gt; in your browser. Drag receipts in (or click &lt;strong&gt;Try sample receipts&lt;/strong&gt;), fix anything the model got wrong, then download the CSV or a ZIP sorted into category folders. The server only listens on your own computer and works offline.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Hasalawa/receiptsorter/docs/screenshot.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHasalawa%2Freceiptsorter%2FHEAD%2Fdocs%2Fscreenshot.png" alt="ReceiptSorter web UI"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Photos of receipts&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Phone photos are read by a local &lt;strong&gt;vision model&lt;/strong&gt;, so you don't need OCR software. Install one once:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull qwen2.5vl:7b&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Photos can take up to a minute each on a laptop without a GPU. If no vision model is installed, the app falls back to Tesseract OCR when it is available…&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/Hasalawa/receiptsorter" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;The open pieces are what make it work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama + &lt;code&gt;qwen2.5:7b-instruct&lt;/code&gt;&lt;/strong&gt; reads text and PDF receipts. I pass a JSON schema through Ollama's structured output (&lt;code&gt;format&lt;/code&gt;), so the model has to return exactly the fields I need and a category from a fixed list. That removes the fragile "parse whatever the model said" step. Temperature is 0 so the same receipt gives the same answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama + &lt;code&gt;qwen2.5vl:7b&lt;/code&gt;&lt;/strong&gt;, an open vision model, reads &lt;strong&gt;photos directly&lt;/strong&gt;. Most real receipts are phone photos, and a vision model avoids the OCR step entirely. Big photos are rotated correctly and shrunk first (Pillow) so a laptop without a GPU stays usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompting that matters for receipts:&lt;/strong&gt; the total is the final amount payable (the "TOTAL BILL" line), never the cash handed over or the change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three fallbacks so the tool never dead-ends:&lt;/strong&gt; vision model, then Tesseract OCR if it is installed, then plain regex rules if Ollama is not running. When something is missing, the error says exactly what to install (for example &lt;code&gt;ollama pull qwen2.5vl:7b&lt;/code&gt;), not just "failed".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small local web UI:&lt;/strong&gt; a standard-library Python server and one HTML file, with no frameworks and no CDN, so it works offline. It listens on &lt;code&gt;127.0.0.1&lt;/code&gt; only and rejects requests with a foreign &lt;code&gt;Host&lt;/code&gt; or &lt;code&gt;Origin&lt;/code&gt;, so a random website cannot talk to it. Every cell in the results table is editable, and the totals and downloads update as you fix things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality checks:&lt;/strong&gt; 17 unit tests using a fake Ollama server (including the vision path and the "model not installed" message), a UI test in a real browser, and a GitHub Actions workflow that runs them on Python 3.10 and 3.12.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data stays on the machine.&lt;/strong&gt; Receipts reveal clients, prices and spending habits. With a closed API I would have to send every image to someone else's server. With local open-weight models I can honestly say nothing leaves the laptop, and it keeps working with no internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The economics work for a side hustle.&lt;/strong&gt; A hosted vision API charges per image, which punishes exactly the customer with a big monthly pile of receipts. Local inference costs nothing per receipt, so a flat price or a one-off setup service can have real margin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap and tune.&lt;/strong&gt; The model is one flag (&lt;code&gt;--model&lt;/code&gt;, &lt;code&gt;--vision-model&lt;/code&gt;). If a bigger model, a smaller one or a model fine-tuned on local receipts (Sri Lankan vendors and LKR amounts, for example) works better, I change a string. I am not tied to someone else's pricing, rate limits or deprecation schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open pieces made the product, not just the backend.&lt;/strong&gt; Ollama's structured output, an open vision model and plain Python were enough to build the whole thing, with nothing to sign up for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I have not verified yet
&lt;/h2&gt;

&lt;p&gt;I tested the code paths thoroughly with sample receipts and a fake Ollama server, and ran a real receipt photo through the UI using the OCR fallback. I have not yet checked the vision model's accuracy on real receipts, and photos can take up to a minute each on a CPU-only laptop. Next steps are an accuracy check on real receipts, a per-vendor "learn my categories" setting, and batch speed-ups.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Wihanga Nada: an offline bird-call identifier for Sri Lanka</title>
      <dc:creator>Kehan Hasalawa</dc:creator>
      <pubDate>Sat, 10 Oct 2026 13:38:01 +0000</pubDate>
      <link>https://dev.to/hasalawa/wihanga-nada-an-offline-bird-call-identifier-for-sri-lanka-23fp</link>
      <guid>https://dev.to/hasalawa/wihanga-nada-an-offline-bird-call-identifier-for-sri-lanka-23fp</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;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Wihanga Nada ("sounds of birds") is a bird-call identifier for Sri Lanka that works with no signal and no account. You tap &lt;strong&gt;Listen&lt;/strong&gt; once, put the phone away, and look up. It listens in 6-second pieces, says the bird's name out loud, flags Sri Lankan endemics, and keeps a trip log you can export as CSV.&lt;/p&gt;

&lt;p&gt;The goal was to make the screen the shortest part of the experience. If the app does its job, you spend the walk watching the canopy, not reading a phone. The best birding places here (Sinharaja, Horton Plains, Yala) are also the places with the worst mobile signal, so "works offline" is not a nice-to-have.&lt;/p&gt;

&lt;p&gt;It is for anyone who wants to put names to the sounds around them, from first-time walkers to people who already carry binoculars.&lt;/p&gt;

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

&lt;p&gt;There is no deployed link, on purpose: the whole point is that it runs on your own machine without the internet. To try it in two minutes, with no TensorFlow and no model download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements-dev.txt
python run.py &lt;span class="nt"&gt;--demo&lt;/span&gt;
&lt;span class="c"&gt;# open http://127.0.0.1:5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Demo mode returns canned detections so you can see the interface. It does not listen to the audio. The real identifier is &lt;code&gt;python run.py&lt;/code&gt; after installing &lt;code&gt;requirements.txt&lt;/code&gt; and &lt;code&gt;tensorflow&lt;/code&gt;.&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/Hasalawa" rel="noopener noreferrer"&gt;
        Hasalawa
      &lt;/a&gt; / &lt;a href="https://github.com/Hasalawa/wihanga-nada" rel="noopener noreferrer"&gt;
        wihanga-nada
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Offline bird-call identifier for Sri Lanka. Runs BirdNET locally, flags endemic species, speaks names aloud so you can look up instead of at a screen. Optional local LLM field notes via Ollama. No internet, no account.
    &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;Wihanga Nada (විහඟ නාද)&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/123665043/670394793-d0da37f5-01ea-4e0d-ae9b-14216081bd0f.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTE2NDE3MjEsIm5iZiI6MTc5MTY0MTQyMSwicGF0aCI6Ii8xMjM2NjUwNDMvNjcwMzk0NzkzLWQwZGEzN2Y1LTAxZWEtNGUwZC1hZTliLTE0MjE2MDgxYmQwZi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYxMDEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MTAxMFQxNDEwMjFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT02N2RkOGUyMDk0NWI3MmNhZjAwZTI3NDJlZjdjNzNhNWEzOGJmOGYwOTZmZGM5MTJmMmYyYjA1MTIzMTMyZWM2JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZyZXNwb25zZS1jb250ZW50LXR5cGU9aW1hZ2UlMkZwbmcifQ.wMJwQkxeyznfXbadhKdpvg7Iq6Fp0M5iegt8EPu9suE"&gt;&lt;img width="2000" height="840" alt="cover" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F123665043%2F670394793-d0da37f5-01ea-4e0d-ae9b-14216081bd0f.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTE2NDE3MjEsIm5iZiI6MTc5MTY0MTQyMSwicGF0aCI6Ii8xMjM2NjUwNDMvNjcwMzk0NzkzLWQwZGEzN2Y1LTAxZWEtNGUwZC1hZTliLTE0MjE2MDgxYmQwZi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYxMDEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MTAxMFQxNDEwMjFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT02N2RkOGUyMDk0NWI3MmNhZjAwZTI3NDJlZjdjNzNhNWEzOGJmOGYwOTZmZGM5MTJmMmYyYjA1MTIzMTMyZWM2JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZyZXNwb25zZS1jb250ZW50LXR5cGU9aW1hZ2UlMkZwbmcifQ.wMJwQkxeyznfXbadhKdpvg7Iq6Fp0M5iegt8EPu9suE" class="js-gh-image-fallback"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An offline bird-call identifier for Sri Lanka. Tap &lt;strong&gt;Listen&lt;/strong&gt;, put the phone away, look up. The app says the bird's name out loud, flags Sri Lankan endemics, and keeps a trip log. Everything runs on your own machine, with no internet and no account.&lt;/p&gt;

&lt;p&gt;Built for the Hacktoberfest Open-Source AI Challenge, week 1: &lt;em&gt;Touch Grass&lt;/em&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt; browser (mic, speech)              your laptop / Raspberry Pi
 ┌──────────────────┐   6 s WAV    ┌──────────────────────────────────────┐
 │ record, resample │ ───────────▶ │ Flask                                │
 │ speak the name   │ ◀─────────── │  ├─ BirdNET 2.4 (TFLite, local)      │
 └──────────────────┘   detections │  ├─ endemics.json (verified facts)   │
                                   │  ├─ Ollama + small LLM (optional)    │
                                   │  └─ SQLite trip log (local file)     │
                                   └──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BirdNET 2.4&lt;/strong&gt; does the actual identification. The model ships &lt;em&gt;inside&lt;/em&gt; the &lt;code&gt;birdnetlib&lt;/code&gt; pip package, so after &lt;code&gt;pip install&lt;/code&gt; it needs no network. GPS (or central Sri Lanka…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Hasalawa/wihanga-nada" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BirdNET 2.4&lt;/strong&gt; (through the &lt;code&gt;birdnetlib&lt;/code&gt; package) does the identification. The model files ship inside the pip package, so after install it needs no network. I pass GPS (or central Sri Lanka as a fallback) and the date, and BirdNET narrows its candidates to species that can occur there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small local LLM through Ollama&lt;/strong&gt; (default &lt;code&gt;gemma3:1b&lt;/code&gt;, swappable with one environment variable) writes the field notes. Small models make things up, so it never answers from memory: it only gets facts from my own &lt;code&gt;endemics.json&lt;/code&gt; and is told to add nothing else. If Ollama isn't running, the app falls back to a static note built from the same facts, so it never depends on the LLM. Birds that aren't in my verified list are never sent to the model at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flask and a plain web page.&lt;/strong&gt; The page records the microphone, switches off the browser's voice processing (echo cancellation and noise suppression smear bird calls), encodes WAV chunks, and speaks the results with the browser's built-in speech synthesis. A screen wake lock and a dim "pocket mode" keep it listening while you walk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; for the trip log, with de-duplication so one bird singing for a minute is one sighting, not ten.&lt;/li&gt;
&lt;li&gt;I built it with Claude as my coding assistant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I checked: BirdNET's own label file (to see which Sri Lankan endemics it can actually name), that the real BirdNET path runs end to end on synthetic audio at both 44.1 and 48 kHz (in well under a second per chunk on my test machine), that the HTTP and HTTPS servers respond, and 14 automated tests.&lt;/p&gt;

&lt;p&gt;Limits, stated plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BirdNET 2.4 covers only 15 of the 28 endemics in my seed list.&lt;/strong&gt; Hill-country specialists such as the Yellow-eared Bulbul, Dull-blue Flycatcher and Sri Lanka Bush Warbler, and the Serendib Scops-Owl, are not in the model, so the app cannot name them yet. A small script (&lt;code&gt;tools/check_coverage.py&lt;/code&gt;) reports this gap instead of hiding it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I have not yet measured identification accuracy on real recordings in the field.&lt;/strong&gt; My end-to-end run used synthetic silence and noise, which checks the plumbing, not whether it names birds correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile browsers pause the microphone when the screen locks.&lt;/strong&gt; The wake lock and pocket mode work around it, but a native app would do better.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tflite-runtime&lt;/code&gt; has no wheels for Python 3.13 and &lt;code&gt;birdnetlib&lt;/code&gt; needs &lt;code&gt;audioop-lts&lt;/code&gt; there, so the README documents the install path that worked.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No signal, no problem.&lt;/strong&gt; A closed audio-ID API needs a connection, and the best birding spots here have none. Open weights run on the laptop in the bag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data stays with the person.&lt;/strong&gt; Recordings and sightings never leave the machine. That matters when a sighting log can reveal where a rare, poachable bird lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap pieces.&lt;/strong&gt; The LLM is one environment variable. The identifier sits behind a tiny interface, so another model can replace BirdNET.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gaps are fixable.&lt;/strong&gt; With a closed API, a missing species is a feature request. Here, the 13 endemics BirdNET misses are an open to-do: &lt;code&gt;birdnetlib&lt;/code&gt; supports custom classifiers, so a community can train on openly licensed recordings and add them. I haven't done that yet, so I list it as the next step, not a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No API key and no per-call fee.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest caveat: the BirdNET model files are CC BY-NC-SA 4.0. They are open weights, but non-commercial. That suits a community hiking tool and not a paid product, and anyone building on this should know it.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Matara Garden: an offline weekly garden checklist, with Gemma on my laptop</title>
      <dc:creator>Kehan Hasalawa</dc:creator>
      <pubDate>Thu, 08 Oct 2026 22:45:49 +0000</pubDate>
      <link>https://dev.to/hasalawa/matara-garden-an-offline-weekly-garden-checklist-with-gemma-on-my-laptop-4596</link>
      <guid>https://dev.to/hasalawa/matara-garden-an-offline-weekly-garden-checklist-with-gemma-on-my-laptop-4596</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;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Matara Garden is a weekly checklist for home gardeners in Matara, Sri Lanka. You open it once, see what is ready to harvest, what suits sowing this month and what the rain means for your beds, then put the phone down and go do those things. Ticking them off is the only screen time left.&lt;/p&gt;

&lt;p&gt;It has three lists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Harvest&lt;/strong&gt;: crops in your garden log that will be ready within a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sow and plant&lt;/strong&gt;: crops that suit this month's rain. Ticking one adds it to your garden with today's date, so the app can tell you when it is ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Care&lt;/strong&gt;: jobs matched to the month's rainfall, like clearing drains, staking climbers or mulching in a dry week. These reset every week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The header has a small rain gauge, because in Matara the rain decides everything. October is "very wet" in my data, so the app tells you to clear the drains and keep rain-sensitive crops back instead of telling you to water.&lt;/p&gt;

&lt;p&gt;At the top there is a short note from Gemma, in English or Sinhala, that sums up the week.&lt;/p&gt;

&lt;p&gt;It is for anyone with a few beds, grow bags or a kitchen garden in the southern wet zone who keeps asking "what should I actually do this week?" and doesn't want another app account.&lt;/p&gt;

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

&lt;p&gt;The app on a laptop, with a few jobs already ticked off (the screenshots use an example garden log):&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%2Feazar9qtbco0s0t3ogpi.webp" 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%2Feazar9qtbco0s0t3ogpi.webp" alt=" " width="800" height="1210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same page on a phone, which is where it gets used:&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%2Fhjljdirj9kf4doi36i6h.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%2Fhjljdirj9kf4doi36i6h.png" alt=" " width="725" height="1568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"My garden" is where you log what is growing. Each crop shows how far it is from harvest:&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%2Fek5kwoq8vsny4af9fjsw.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%2Fek5kwoq8vsny4af9fjsw.png" alt=" " width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking It Outside
&lt;/h2&gt;

&lt;p&gt;An app like this only works if the screen stays short, so I tried it the way it is meant to be used. The video above shows it: I open the checklist on my phone, read what this week needs, go and do those jobs in the garden, and tick each one when it is actually finished.&lt;/p&gt;

&lt;p&gt;The phone talks to the laptop over home Wi-Fi (&lt;code&gt;python server.py --host 0.0.0.0&lt;/code&gt;), so the phone needs no mobile data and the laptop needs no internet. Ticks are saved the moment I tap, so there is no save button to remember.&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/Hasalawa" rel="noopener noreferrer"&gt;
        Hasalawa
      &lt;/a&gt; / &lt;a href="https://github.com/Hasalawa/matara-garden" rel="noopener noreferrer"&gt;
        matara-garden
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Offline weekly garden checklist for Matara, Sri Lanka. Tick off what to harvest, sow and care for, with a weekly note from local Gemma via Ollama.
    &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;Matara Garden&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A weekly garden checklist for home gardeners in Matara, Sri Lanka. Open it, see what to harvest, sow and look after this week, and tick things off as you do them outside. The screen is the shortest part of the day.&lt;/p&gt;
&lt;p&gt;It runs on your own laptop, offline. A small rule-based crop calendar decides what needs doing (so dates and crops are never made up), and a local open-weight model, Gemma through Ollama, writes the short weekly note at the top.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run it&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Install &lt;a href="https://www.python.org/downloads/" rel="nofollow noopener noreferrer"&gt;Python&lt;/a&gt; (tick "Add python.exe to PATH") and &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Download the model once: &lt;code&gt;ollama pull gemma3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In this folder run: &lt;code&gt;python server.py&lt;/code&gt; (on Windows you can also double-click &lt;code&gt;start.bat&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your browser opens at &lt;a href="http://localhost:8000" rel="nofollow noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;. To open it on your phone, run &lt;code&gt;python server.py --host 0.0.0.0&lt;/code&gt; and visit &lt;code&gt;http://&amp;lt;your-laptop-ip&amp;gt;:8000&lt;/code&gt; on the same Wi-Fi.&lt;/p&gt;
&lt;p&gt;No packages to install: the app uses only Python's standard…&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/Hasalawa/matara-garden" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;To run it:&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 gemma3
python server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses only Python's standard library, so there is nothing to &lt;code&gt;pip install&lt;/code&gt;. On Windows you can double-click &lt;code&gt;start.bat&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;The stack is Python's standard library, Ollama, Gemma 3, one HTML page and two JSON files. No pip, no npm, no build step.&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%2Fca8fijw2pt66wzxsbr35.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%2Fca8fijw2pt66wzxsbr35.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The design came out of a mistake. My first version gave Gemma the crop data and asked it to write the whole checklist. It worked offline, but I wouldn't trust the output in a garden. It opened with "Okay, here's your Matara garden checklist", wrapped headings in markdown asterisks, and told me to stake brinjal and chili plants that were not in my garden, because it blended tip lines from the data into its advice. A small model with loose grounding will do that.&lt;/p&gt;

&lt;p&gt;So I split the job in two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rules decide what to do.&lt;/strong&gt; &lt;code&gt;crops.json&lt;/code&gt; holds sowing months, days to harvest and rain tolerance for each crop, plus a rain level for each month. Plain Python combines it with &lt;code&gt;garden.json&lt;/code&gt; to build the three lists. Dates and crops can't be invented, because the model never produces them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma only writes the note.&lt;/strong&gt; It gets a short JSON of this week's facts and writes two or three sentences. The prompt forbids new facts, and a cleanup step strips any intro line and markdown. If Ollama isn't running, the checklist still works and the note says why it is off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is deliberately small. A local server built on &lt;code&gt;http.server&lt;/code&gt; exposes a tiny JSON API, and one HTML page talks to it. Ticks are saved into &lt;code&gt;garden.json&lt;/code&gt; through the server, so progress survives a restart and isn't locked inside one browser. The server rejects non-JSON posts and unknown &lt;code&gt;Host&lt;/code&gt; headers, so another website open in your browser can't poke at your garden.&lt;/p&gt;

&lt;p&gt;There are 23 unit tests, and a GitHub Actions workflow runs them on Python 3.9, 3.11 and 3.13. It also builds the checklist for all 12 months and starts the server to check it serves the page. None of it needs Ollama.&lt;/p&gt;

&lt;p&gt;I pair-programmed this with Claude. Most of the code was written together, and I ran, read and tested it myself.&lt;/p&gt;

&lt;p&gt;Two limits to be upfront about. First, &lt;code&gt;crops.json&lt;/code&gt; is starter data written from general knowledge, not an agricultural reference. The app says so in its footer, and the README asks gardeners to check sowing windows with their local Agrarian Service Centre. Second, the Sinhala note is a one-line change to the prompt, but small models are usually less fluent in Sinhala than in English, so treat that note as a draft and have a Sinhala reader check it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The garden is where the signal is worst.&lt;/strong&gt; Gemma runs through Ollama on the laptop, so the whole app works with Wi-Fi off. A hosted API would turn it into a page that fails exactly where it is needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My garden is nobody else's data.&lt;/strong&gt; Where I live, what I planted and when stay in a JSON file on my machine. There is no account, no API key and no billing page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; A weekly note for a home garden is not something I want to pay per call for, or manage a key for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can change anything.&lt;/strong&gt; The model is one &lt;code&gt;--model&lt;/code&gt; flag. The Sinhala note is a single line in the prompt. &lt;code&gt;crops.json&lt;/code&gt; is a plain file, so a gardener in Galle or Kandy can copy the project, change the rainfall levels and the sowing months, and have a planner for their own district. That is hard to do with a service you only reach through someone else's API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It also showed me the limits.&lt;/strong&gt; A small local model is not a frontier model, and my first version proved it. Because the whole pipeline was in front of me, I could see exactly where the model went wrong and decide it should only do the part it is good at: friendly wording on top of facts that code has already checked.&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 runs locally through Ollama and writes the weekly note in English or Sinhala, while rule-based code keeps it grounded.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>AI English Practice Buddy – Built for a Friend with Gemma &amp; Ollama (Offline)</title>
      <dc:creator>Kehan Hasalawa</dc:creator>
      <pubDate>Sun, 04 Oct 2026 18:20:07 +0000</pubDate>
      <link>https://dev.to/hasalawa/ai-english-practice-buddy-built-for-a-friend-with-gemma-ollama-offline-1l8n</link>
      <guid>https://dev.to/hasalawa/ai-english-practice-buddy-built-for-a-friend-with-gemma-ollama-offline-1l8n</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 built &lt;strong&gt;AI English Buddy&lt;/strong&gt; for a friend of mine who is currently improving their English. Grammar is the hard part: they often write a sentence that feels right but isn't, and a corrected version alone doesn't help because they still don't know &lt;em&gt;why&lt;/em&gt; it was wrong.&lt;/p&gt;

&lt;p&gt;I wanted something they could use any time, without paying for a subscription or worrying about someone reading their mistakes. So the app takes any sentence they type and gives back the corrected version, a short explanation in simple words, a more natural way to say it, and one new example using the same rule.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grammar check:&lt;/strong&gt; tells them if the sentence is correct and gives the fixed version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain my mistake:&lt;/strong&gt; a short explanation in simple words&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better version:&lt;/strong&gt; a more natural, fluent way to say the same thing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example:&lt;/strong&gt; a fresh sentence using the same grammar rule
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You:   I am go to university yesterday.

Buddy: ❌ Your sentence needs correction.
       ✅ I went to university yesterday.
       💡 "Yesterday" talks about the past, so use "went", not "go".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/LUgVFDa4KMo" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I shared the app with my friend and asked for honest feedback. Here is what they said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Great work! This is a really nice and useful project for learning English. I especially like how it doesn't just correct the sentence, but also explains the mistake and gives a better version and an example. The interface is simple and easy to understand.&lt;/p&gt;

&lt;p&gt;I also really like the idea of running it locally with Gemma via Ollama, because it keeps the user's sentences private and doesn't require a subscription."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They also suggested what to add next: vocabulary suggestions, pronunciation practice, and a history of previous corrections.&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%2Fn6b34wke5huhyextrnfz.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%2Fn6b34wke5huhyextrnfz.jpg" alt=" " width="562" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;My friend's feedback gave me a clear list to build on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correction history:&lt;/strong&gt; save previous sentences and fixes so they can review their common mistakes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary suggestions:&lt;/strong&gt; suggest richer words for the same idea&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pronunciation practice:&lt;/strong&gt; let them hear and practice the corrected sentence&lt;/li&gt;
&lt;/ul&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/Hasalawa" rel="noopener noreferrer"&gt;
        Hasalawa
      &lt;/a&gt; / &lt;a href="https://github.com/Hasalawa/ai-english-practice-buddy" rel="noopener noreferrer"&gt;
        ai-english-practice-buddy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An AI English practice buddy built for a friend learning English. Type a sentence and get a correction, a simple explanation, a more natural version, and an example. Runs privately with Google's open-weight Gemma model via Ollama, using a Flask backend and plain HTML/CSS/JS frontend.
    &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;AI English Practice Buddy&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Hasalawa/ai-english-practice-buddy/actions/workflows/ci.yml/badge.svg"&gt;&lt;img src="https://github.com/Hasalawa/ai-english-practice-buddy/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An AI-powered English practice tool built for my friend, who is improving their English.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grammar check&lt;/strong&gt; – type a sentence, get the corrected version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain my mistake&lt;/strong&gt; – a short, simple explanation of why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better version&lt;/strong&gt; – a more natural way to say it, plus an example sentence&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;AI&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Uses the open-weight &lt;strong&gt;Gemma&lt;/strong&gt; model (&lt;code&gt;gemma3:1b&lt;/code&gt;) running locally through &lt;strong&gt;Ollama&lt;/strong&gt;.
Why open models? Sentences stay on your laptop (privacy), there is no API cost, and you can swap the model with one env var.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tech stack&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;HTML · CSS · JavaScript · Python (Flask) · Ollama · Gemma&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run it&lt;/h2&gt;

&lt;/div&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; 1. Install Ollama (https://ollama.com) and pull the model&lt;/span&gt;
ollama pull gemma3:1b

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; 2. Install and start the app&lt;/span&gt;
pip install -r requirements.txt
python app.py&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://localhost:5000" rel="nofollow noopener noreferrer"&gt;http://localhost:5000&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Optional env vars: &lt;code&gt;OLLAMA_MODEL&lt;/code&gt; (default &lt;code&gt;gemma3:1b&lt;/code&gt;), &lt;code&gt;OLLAMA_URL&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;Browser -&amp;gt; POST /check&lt;/code&gt;&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/Hasalawa/ai-english-practice-buddy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;The AI at the core is &lt;strong&gt;Gemma&lt;/strong&gt; (&lt;code&gt;gemma3:1b&lt;/code&gt;), Google's open-weight model, running &lt;strong&gt;locally through Ollama&lt;/strong&gt;. Nothing is sent to an external AI API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser -&amp;gt; POST /check -&amp;gt; Flask -&amp;gt; Ollama -&amp;gt; Gemma -&amp;gt; JSON -&amp;gt; Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; a single page in plain HTML, CSS and JavaScript. No framework, no login, no database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; a small Flask app with one endpoint, &lt;code&gt;POST /check&lt;/code&gt;. It validates the input (non-empty, max 500 characters) and calls Ollama's chat API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompting:&lt;/strong&gt; a system prompt makes Gemma act as a patient English teacher and reply &lt;em&gt;only&lt;/em&gt; as JSON with &lt;code&gt;is_correct&lt;/code&gt;, &lt;code&gt;corrected&lt;/code&gt;, &lt;code&gt;explanation&lt;/code&gt;, &lt;code&gt;example&lt;/code&gt; and &lt;code&gt;better&lt;/code&gt;. Ollama's &lt;code&gt;format: "json"&lt;/code&gt; option plus a low temperature (0.2) keeps the output consistent, so the UI can render each part separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling:&lt;/strong&gt; if Ollama isn't running, the app says so instead of crashing (503). If the model returns something unparseable, the user gets a friendly retry message (502).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests and CI:&lt;/strong&gt; pytest tests cover the endpoint, with the Ollama call mocked. A GitHub Actions workflow runs the linter (ruff) and the tests on every push, so CI doesn't need a model at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest challenge was that a 1B model is small, and a small model can easily ignore formatting instructions. My UI needs the correction, the explanation, the example and the better version as separate pieces, so a free-form reply wouldn't work. I solved this in three ways: asking Ollama for JSON output, keeping the temperature low (0.2) so the answers stay stable, and writing the backend so that a broken or unparseable reply gives the user a friendly "please try again" message instead of crashing the page. Because the model can't run in CI, my tests mock the Ollama call, so they check that the app handles good and bad replies, but not how good the grammar advice is. For that, the real test is my friend using it.&lt;/p&gt;

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

&lt;p&gt;This is the part that made me choose an open model instead of a hosted API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy:&lt;/strong&gt; practice sentences are personal, and sometimes embarrassing. With a local model they never leave my friend's laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline, costs nothing:&lt;/strong&gt; no API key, no per-request bill, no internet needed once the model is downloaded. That matters for a learner who just wants to practice anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable:&lt;/strong&gt; the model is one environment variable (&lt;code&gt;OLLAMA_MODEL&lt;/code&gt;). If a bigger Gemma, or a different open model, handles grammar better, it's a one-line change and no code rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunable for one person:&lt;/strong&gt; because I control the prompt and the model, I can shape the "teacher" around exactly what this friend struggles with, instead of settling for a generic product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed API could probably give better corrections out of the box, but it couldn't have given me all of the above for a tool built for one friend.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; the whole app is built around Gemma 3 running locally via Ollama.&lt;/p&gt;

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