<?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: Simran Kaur</title>
    <description>The latest articles on DEV Community by Simran Kaur (@fever_dreams).</description>
    <link>https://dev.to/fever_dreams</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%2F4153923%2Fc3d9965f-82d6-4745-a582-23fb224cf490.jpg</url>
      <title>DEV Community: Simran Kaur</title>
      <link>https://dev.to/fever_dreams</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fever_dreams"/>
    <language>en</language>
    <item>
      <title>Department of Unnecessary Affairs: I built a government office to help me avoid doing things</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:31:00 +0000</pubDate>
      <link>https://dev.to/fever_dreams/department-of-unnecessary-affairs-i-built-a-government-office-to-help-me-avoid-doing-things-4b6i</link>
      <guid>https://dev.to/fever_dreams/department-of-unnecessary-affairs-i-built-a-government-office-to-help-me-avoid-doing-things-4b6i</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;p&gt;I had a problem. I had a list of things to do, most of which I did not particularly want to do, and no shortage of ways to reorganise that list instead of actually doing anything.&lt;/p&gt;

&lt;p&gt;I decided to build an application for that.&lt;/p&gt;

&lt;p&gt;It's called the Department of Unnecessary Affairs (DUA). You bring it your administrative problems, and it directs them to the appropriate office. There are five offices, each handling a different kind of problem, and one very relaxed Sloth Officer.&lt;/p&gt;

&lt;p&gt;The Sloth Officer looks a little...&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%2Fdfjmaykj1jwq7jcmtjtc.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%2Fdfjmaykj1jwq7jcmtjtc.png" alt="Zoomed out image of Officer S.Loth" width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;...well. Let's not make this about his appearance.&lt;/p&gt;

&lt;p&gt;Behind the questionable personnel decisions is a small Django application that uses Gemma to handle everyday administrative tasks. The idea is ridiculous. The engineering, considerably less so.&lt;/p&gt;

&lt;p&gt;To be clear, I am not claiming that DUA can do things ChatGPT or Claude cannot. Most of its tasks can already be handled by existing AI assistants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The main objective was to make a troll of an application.&lt;/strong&gt; Giving mundane problems an entire government department, complete with official verdicts, fictional permits, unnecessary paperwork, and a Sloth Officer, seemed funnier than building yet another serious productivity tool.&lt;/p&gt;

&lt;p&gt;Naturally, I then spent a questionable amount of time making sure the joke actually worked.&lt;/p&gt;

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

&lt;p&gt;DUA gives everyday administrative problems their own government office. Need to organise your notes? There's an office for that. Overwhelmed by your to-do list? Another office will issue its verdict. Difficult email to write? Complicated document to understand? Minor decision to outsource to a fictional government? There's a department for each.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Morning Docket Office
&lt;/h3&gt;

&lt;p&gt;Paste your notes, to-do list, or chat export. You can also upload &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt;, and text-based &lt;code&gt;.pdf&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;The office identifies your three most important tasks, what can wait, and what is coming up. Each item includes a quotation from your original notes, so you can check where the recommendation came from.&lt;/p&gt;

&lt;p&gt;For a test, I exported a WhatsApp conversation with a friend. She had asked me to call her the next time I came online and had also asked when IGNOU assignments were due. I had responded to neither question.&lt;/p&gt;

&lt;p&gt;The office had some paperwork to do.&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%2F7wnfxnh3rfyrypuihp5e.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%2F7wnfxnh3rfyrypuihp5e.png" alt="Text messages with one of my friends" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I uploaded the exported conversation:&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%2Fyaoz12v8nxa3c2ssya2o.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%2Fyaoz12v8nxa3c2ssya2o.png" alt="Uploaded chat" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And got these results:&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%2Fg0x70jyolnic8umh03tn.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%2Fg0x70jyolnic8umh03tn.png" alt="Results from the Morning Docket Office" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It identified both unanswered messages as immediate action items. It also brought up a keychain discussion from earlier in the conversation, because apparently that matter was still pending.&lt;/p&gt;

&lt;p&gt;The office also generates a &lt;strong&gt;Notice of Daily Obligations&lt;/strong&gt;, which is essentially a ridiculously legalistic summary of your request and its response.&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%2Fxz135xwb1iu1rvjehi9u.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%2Fxz135xwb1iu1rvjehi9u.png" alt="Notice of Daily Obligations" width="798" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Bureau of Pending Affairs
&lt;/h3&gt;

&lt;p&gt;Give it a list of tasks, and each task receives one of five verdicts: &lt;code&gt;do_now&lt;/code&gt;, &lt;code&gt;schedule&lt;/code&gt;, &lt;code&gt;delegate&lt;/code&gt;, &lt;code&gt;defer&lt;/code&gt;, or &lt;code&gt;deny&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Bureau suggests a next step and the minimum effort needed to consider the task complete. Deferred and denied tasks come with a procrastination permit and a fictional excuse.&lt;/p&gt;

&lt;p&gt;You can mark tasks as complete, track progress, and carry unfinished tasks into a new plan. Only the remaining tasks are replanned, so completed tasks do not return for another hearing.&lt;/p&gt;

&lt;p&gt;There is one restriction: the Bureau cannot talk you out of a real deadline just because ignoring it would be convenient. That rule is enforced in Python.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  3. Office of Correspondence Avoidance
&lt;/h3&gt;

&lt;p&gt;This office handles messages you have been meaning to reply to.&lt;/p&gt;

&lt;p&gt;Enter your rough notes, choose a tone (professional, friendly, firm, apologetic, or casual), and get a draft with two alternatives.&lt;/p&gt;

&lt;p&gt;The more interesting feature is reply mode. Paste an incoming message, and the office identifies its questions using the original wording. Each question is marked as answered in the draft or still needing your input.&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%2F47o3yac727o0ftkpwmi2.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%2F47o3yac727o0ftkpwmi2.png" alt="Correspondence draft" width="706" height="905"&gt;&lt;/a&gt;&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%2Foefmlg8zjfwkhoxicj6p.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%2Foefmlg8zjfwkhoxicj6p.png" alt="Reply mode" width="700" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you haven't provided a name, price, or other specific detail, the application uses a placeholder instead of letting the model invent one.&lt;/p&gt;

&lt;p&gt;A confidently written email containing a date you never agreed to is not particularly helpful. Even the Department has some standards.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Commission for Unnecessarily Complicated Documents
&lt;/h3&gt;

&lt;p&gt;Paste a notice, lease, or terms and conditions, or upload a document.&lt;/p&gt;

&lt;p&gt;The Commission produces a plain-language summary and identifies deadlines, actions, requirements, and unclear points. Each claim needs a quotation from the original document, and Python checks those quotations before the claims are displayed.&lt;/p&gt;

&lt;p&gt;The word count, average sentence length, and reading level are calculated from the actual text. The complexity rating is fictional and labelled accordingly. At least one department is honest about the nature of its assessments.&lt;/p&gt;

&lt;p&gt;The full document is not stored. Only a 200-character preview is retained.&lt;/p&gt;

&lt;p&gt;For a demo, I uploaded a PDF about &lt;em&gt;Latent Space and the Geometry of AI Representations&lt;/em&gt;. The application returned a short summary and key points from the document.&lt;/p&gt;

&lt;p&gt;Since the PDF did not contain any direct action items for me, the deadlines section was empty. Under "What you are asked to do", it extracted these points:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Test the actual downstream task when evaluating representations.&lt;/p&gt;

&lt;p&gt;Source in your document: “A sensible evaluation should test the actual downstream task, inspect failure cases, and check performance across different groups and settings.”&lt;/p&gt;

&lt;p&gt;Treat similarity scores as model-dependent signals.&lt;/p&gt;

&lt;p&gt;Source in your document: “Similarity scores should be treated as model-dependent signals, not objective measurements of truth or meaning.”&lt;/p&gt;
&lt;/blockquote&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%2Fxaejk8mctw7ajllhdovd.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%2Fxaejk8mctw7ajllhdovd.png" alt="Document analysis results" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also got a rating of how complicated the document was:&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%2Fn0zwfw78ev3z44ivi51a.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%2Fn0zwfw78ev3z44ivi51a.png" alt="Document complexity rating" width="800" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Was a separate government commission necessary to summarise a PDF? Almost certainly not. But now it exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Tribunal for Minor Inconveniences
&lt;/h3&gt;

&lt;p&gt;Pizza or sushi? Reply tonight or tomorrow?&lt;/p&gt;

&lt;p&gt;Tell the Tribunal what matters to you, and it produces two to five options, their pros and cons, a recommendation based on your stated preferences, and a mock ruling.&lt;/p&gt;

&lt;p&gt;You can appeal the decision. The same judge hears the appeal and dismisses it.&lt;/p&gt;

&lt;p&gt;The Tribunal has limits. Questions involving health, law, money, or safety are redirected without the jokes. Inputs indicating self-harm are intercepted before they reach the model and receive a fixed, supportive response.&lt;/p&gt;

&lt;p&gt;The application is meant to be absurd about minor inconveniences, not to pretend a local language model should be making high-stakes decisions for someone.&lt;/p&gt;

&lt;h3&gt;
  
  
  A few things shared across the offices
&lt;/h3&gt;

&lt;p&gt;Every completed case has a page that can be revisited. Cases belong to the visitor's browser session, and there is a button to delete a case.&lt;/p&gt;

&lt;p&gt;You can also retry a case if the model produces a bad result.&lt;/p&gt;

&lt;p&gt;Completed cases can be downloaded as Markdown, with the source quotations preserved. Pending plans can be exported as &lt;code&gt;.ics&lt;/code&gt; files for calendar and task applications.&lt;/p&gt;

&lt;p&gt;The calendar exports deliberately have no due dates. If your task says "finish this by Friday", the application preserves that wording in the task description rather than guessing which calendar date Friday refers to.&lt;/p&gt;

&lt;p&gt;And now, the part that is probably more consistent with the original vision:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Department's questionable personnel and activities
&lt;/h3&gt;

&lt;p&gt;There are eight hidden secrets around the application.&lt;/p&gt;

&lt;p&gt;Type &lt;code&gt;sloth&lt;/code&gt; anywhere on a page without a text box selected, and the Sloth Officer walks across the screen at his own pace.&lt;/p&gt;

&lt;p&gt;Alternatively, enter the Konami code:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;↑ ↑ ↓ ↓ ← → ← → B A&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The Department initiates an Emergency Efficiency Drill. Stamps rain down the screen, a banner announces that everyone must work faster, and the drill is cancelled because nobody is interested.&lt;/p&gt;

&lt;p&gt;There are six other secrets. I will leave those to the public.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://dua-demo.onrender.com" rel="noopener noreferrer"&gt;https://dua-demo.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&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/arkia09" rel="noopener noreferrer"&gt;
        arkia09
      &lt;/a&gt; / &lt;a href="https://github.com/arkia09/DUA" rel="noopener noreferrer"&gt;
        DUA
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Department of Unnecessary Affairs&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A Django app where you hand a fictional, deadpan government office a problem and get real help back. A local
open-weight model (&lt;code&gt;gemma4:e4b&lt;/code&gt;, served by &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;) does the thinking. Nothing you type leaves
your machine.&lt;/p&gt;
&lt;p&gt;The rule behind every office: &lt;strong&gt;the help is serious and the jokes are separate.&lt;/strong&gt; Plans, drafts, summaries and
comparisons are plain and useful. The humour lives in clearly labelled notices, excuses and stamps.&lt;/p&gt;
&lt;p&gt;There is also a &lt;strong&gt;demo mode&lt;/strong&gt; that needs no model, so the app can be hosted as a public demo. See
&lt;a href="https://github.com/arkia09/DUA#demo-mode" rel="noopener noreferrer"&gt;Demo mode&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The five offices&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Office&lt;/th&gt;
&lt;th&gt;You give it&lt;/th&gt;
&lt;th&gt;You get back&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Morning Docket Office&lt;/strong&gt; (&lt;code&gt;/briefing/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A pile of notes, to-dos, messages or a chat export (paste, &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt; or text-based &lt;code&gt;.pdf&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Today's top three, what can safely wait, what is coming up, each item with a quote from&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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/arkia09/DUA" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The hosted demo is designed to be tried without installing anything.&lt;/p&gt;

&lt;p&gt;A quick way to explore it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the Bureau of Pending Affairs.&lt;/li&gt;
&lt;li&gt;Select one of the built-in &lt;code&gt;Try&lt;/code&gt; examples and submit it.&lt;/li&gt;
&lt;li&gt;Mark a task as complete.&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;.ics&lt;/code&gt; to-do list.&lt;/li&gt;
&lt;li&gt;Enter the Konami code if you want to see the Emergency Efficiency Drill.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One important distinction: &lt;strong&gt;the hosted demo does not run Gemma.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It has 12 built-in sample inputs with saved model outputs. When you submit one of those samples, the application processes the saved output through the same validation functions used for live model responses.&lt;/p&gt;

&lt;p&gt;If you enter your own input, the demo cannot process it and returns a refusal.&lt;/p&gt;

&lt;p&gt;This lets people try the application without requiring the hosting server to run an AI model. It also means the demo exercises the validation code rather than displaying pre-rendered results without running the application logic.&lt;/p&gt;

&lt;p&gt;To run the full application with live inference, you need Ollama and Gemma on your own machine:&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 gemma4:e4b
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python manage.py migrate
python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;DEMO_MODE=true&lt;/code&gt; in your environment if you want to run the application without Ollama.&lt;/p&gt;

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

&lt;p&gt;The stack is Django, SQLite, plain JavaScript without a frontend framework, and Gemma 4 E4B served locally through Ollama.&lt;/p&gt;

&lt;p&gt;The division of responsibilities is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Django&lt;/strong&gt; handles HTTP requests, application logic, sessions, and server-side processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; stores application records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt; handles the browser interfaces and interactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; runs Gemma locally and provides the interface through which the application sends prompts and receives responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 E4B&lt;/strong&gt; handles the language tasks for the five offices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a user submits something, the relevant Django view prepares the input and sends it to the appropriate model operation. The model returns structured JSON. Python validates that output before the application presents it to the user.&lt;/p&gt;

&lt;p&gt;The important part is that valid JSON is not necessarily correct information.&lt;/p&gt;

&lt;p&gt;A response can have every expected field and still contain an invented deadline. It can return a plausible summary and attach a quotation that never appeared in the document.&lt;/p&gt;

&lt;p&gt;I wanted the application to check the parts that could be checked rather than relying entirely on a better prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Gemma proposes. Python decides.
&lt;/h3&gt;

&lt;p&gt;Take the document-analysis office.&lt;/p&gt;

&lt;p&gt;Each claim must include a quotation from the source document. The application normalises the text, checks that the quotation is long enough, and verifies that it appears in the original text.&lt;/p&gt;

&lt;p&gt;The core check looks like this:&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;def&lt;/span&gt; &lt;span class="nf"&gt;quote_in_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MIN_QUOTE_WORDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_chars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_words&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source_words&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The minimum length matters because a short, generic phrase is not useful evidence for a claim about a document.&lt;/p&gt;

&lt;p&gt;If the quotation fails validation, the associated claim is removed, and the interface reports how many claims were rejected.&lt;/p&gt;

&lt;p&gt;This does not prove that the model interpreted a quotation correctly. A real quotation can still be used to support the wrong conclusion.&lt;/p&gt;

&lt;p&gt;It does, however, prevent the application from presenting an invented passage as a verbatim quotation from the source.&lt;/p&gt;

&lt;p&gt;That is a specific safeguard, not a solution to hallucinations as a whole.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Bureau cannot approve every excuse
&lt;/h3&gt;

&lt;p&gt;The Bureau of Pending Affairs has a similar rule.&lt;/p&gt;

&lt;p&gt;Gemma can recommend that a task be deferred or denied, but Python can override that recommendation:&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="c1"&gt;# Safety guard: never let the model excuse someone from a deadline or a high-stakes task.
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verdict&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;defer&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;deny&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;urgency&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;high&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;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;do_now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;urgency&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a task has a deadline or high urgency, the model cannot simply dismiss it. Critical tasks are moved to &lt;code&gt;do_now&lt;/code&gt;, while the other matching tasks are scheduled.&lt;/p&gt;

&lt;p&gt;I chose to enforce this in code rather than rely exclusively on instructions in the prompt. A model may fail to follow an instruction. A deterministic check gives the application a separate way to enforce the rule.&lt;/p&gt;

&lt;p&gt;The application also preserves deadlines from the user's input rather than accepting invented ones. User input is passed as data inside tags, with closing tags neutralised to reduce the risk of the input interfering with the surrounding instructions.&lt;/p&gt;

&lt;p&gt;This is one layer of protection, not a claim that prompt injection has been solved.&lt;/p&gt;

&lt;p&gt;The test suite contains &lt;strong&gt;244 tests&lt;/strong&gt;. The model is mocked during these tests, so they verify how the application handles different outputs without requiring live inference for every test.&lt;/p&gt;

&lt;p&gt;That includes malformed, incomplete, and invalid responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Demo mode uses the same checks
&lt;/h3&gt;

&lt;p&gt;The hosted demo has a separate requirement: it must work without Ollama.&lt;/p&gt;

&lt;p&gt;I implemented demo mode by recording model outputs for 12 built-in sample inputs. The application stores those outputs and uses them when the corresponding examples are submitted.&lt;/p&gt;

&lt;p&gt;The saved responses still pass through the regular validators. A test checks that the sample outputs survive validation and that every &lt;code&gt;Try&lt;/code&gt; button has a corresponding sample.&lt;/p&gt;

&lt;p&gt;I can regenerate the sample data after running the real model locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python manage.py record_demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me a way to update the demo without hosting inference on the public server.&lt;/p&gt;

&lt;p&gt;The distinction matters because a demo that displays saved responses directly would not exercise the same application logic as a live model call. DUA's demo still runs the checks that determine which claims and recommendations are allowed through.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Deploying Django on Render
&lt;/h3&gt;

&lt;p&gt;The public demo runs on &lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I wanted a public URL that people could open immediately, without installing Python, downloading Gemma, or configuring Ollama. Running the model on a hosting server would have introduced additional resource requirements, so I kept the model out of the hosted deployment and used demo mode instead.&lt;/p&gt;

&lt;p&gt;The deployment is a Django web service rather than a static site. Django needs to process requests, manage sessions, run the office logic, and retrieve the saved demo outputs.&lt;/p&gt;

&lt;p&gt;The service runs under Gunicorn. Its build process installs the Python dependencies, collects static files, and applies database migrations:&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.txt
python manage.py collectstatic &lt;span class="nt"&gt;--noinput&lt;/span&gt;
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application starts with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gunicorn dua.wsgi:application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also added a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint to the repository. Render generated the initial configuration for the deployed service, and I committed that configuration so the deployment setup lives alongside the application code.&lt;/p&gt;

&lt;p&gt;That makes the deployment easier to reproduce and review than relying entirely on settings entered manually in a dashboard.&lt;/p&gt;

&lt;p&gt;The service's environment variables handle deployment-specific settings, including demo mode, the allowed host, the Django secret key, and HTTPS-related configuration. Sensitive values belong in Render's environment settings, not in the repository.&lt;/p&gt;

&lt;p&gt;Getting the application to start was not the same as getting it to serve requests correctly. At one point, the service started successfully, but requests returned HTTP 400 because the deployed hostname was missing from Django's allowed-host configuration. I added the hostname and checked the related HTTPS and CSRF settings.&lt;/p&gt;

&lt;p&gt;There is also a deliberate limitation to the hosted setup. The demo does not provide unrestricted AI inference, and the deployed application clears case data when the service restarts. This keeps the public demonstration simple, but it is not a substitute for persistent storage and a more complete data-retention design.&lt;/p&gt;

&lt;p&gt;For this project, I prioritised making the core experience publicly accessible without incurring the cost of hosting the model.&lt;/p&gt;

&lt;p&gt;That is what Render contributes to DUA: a deployed, publicly accessible application with its own server-side logic, rather than a screenshot of an interface that only works on my machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The bugs that changed the implementation
&lt;/h3&gt;

&lt;p&gt;The first serious issue was case ownership.&lt;/p&gt;

&lt;p&gt;In the initial implementation, anyone who knew a case number could open, export, or delete another visitor's case. Case numbers were sequential, and the application did not check ownership before allowing those operations.&lt;/p&gt;

&lt;p&gt;I changed the ownership model so that cases belong to the visitor's browser session. The server also clears cases when it restarts.&lt;/p&gt;

&lt;p&gt;Fixing the ownership problem exposed another issue. When a model call failed, Django returned a 5xx response without sending the session cookie. For a new visitor, that could leave them unable to access their own failed case.&lt;/p&gt;

&lt;p&gt;I added middleware to ensure the session cookie was sent in that situation as well.&lt;/p&gt;

&lt;p&gt;The lesson was that assigning data to a session is not enough if the error-handling path breaks the session flow.&lt;/p&gt;

&lt;p&gt;I also changed Gemma's prompts while trying to make its jokes blunter. The result was that the responses became dull. The legal drama was part of the application's personality, and the revised prompts had stripped too much of it away.&lt;/p&gt;

&lt;p&gt;I restored the earlier wording.&lt;/p&gt;

&lt;p&gt;Sometimes the old version was better.&lt;/p&gt;

&lt;p&gt;These issues were not solved by asking the model to behave better. They required changes to application logic, HTTP handling, and the prompts themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Running the model locally
&lt;/h3&gt;

&lt;p&gt;Ollama makes the local version relatively straightforward to run.&lt;/p&gt;

&lt;p&gt;Once Gemma is downloaded, Django can send requests to the local Ollama service without depending on a paid model API.&lt;/p&gt;

&lt;p&gt;The application uses the &lt;code&gt;OLLAMA_MODEL&lt;/code&gt; environment variable to select the model. That means the model choice can be changed through configuration rather than hard-coded into every office.&lt;/p&gt;

&lt;p&gt;Local inference also has trade-offs. The model needs to be downloaded, the machine needs enough resources to run it, and inference speed depends on the hardware.&lt;/p&gt;

&lt;p&gt;A larger hosted model might produce better summaries or funnier excuses. A local model does not become reliable simply because it runs on your own computer.&lt;/p&gt;

&lt;p&gt;That is why the validation layer exists independently of the model.&lt;/p&gt;

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

&lt;p&gt;I could make a case for the practical advantages of open-source technology here, but I don't think DUA needs to pretend it solves a problem that existing AI assistants have failed to solve.&lt;/p&gt;

&lt;p&gt;You can already ask ChatGPT or Claude to organise your tasks, draft replies, or summarise a document. I haven't benchmarked DUA against those tools, and I have no evidence that it does those jobs better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reason I built DUA was primarily to make a ridiculous application, then see how far I could take the idea technically.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open-source tools made that experiment accessible. I could run Gemma locally through Ollama, change prompts and application logic, inspect the outputs, and build validation rules around the parts I didn't want to trust blindly. Running the model locally also avoids per-request model API charges, although it comes with hardware and setup costs.&lt;/p&gt;

&lt;p&gt;The project is open for other people to inspect and modify. They can disagree with my implementation, change the rules, replace the model, or build something more useful on top of it. That freedom matters more to me than claiming that DUA is a necessary replacement for a general-purpose assistant.&lt;/p&gt;

&lt;p&gt;There is a local-inference option for people who prefer not to send their inputs to a remote model API. The full local version can process inputs through Ollama on the user's machine, subject to the application's actual data flows and configuration. The hosted demo is different: it uses saved sample responses and should not be treated as a privacy guarantee for arbitrary user input.&lt;/p&gt;

&lt;p&gt;Open-source software does not automatically make an application secure, accurate, or useful. It does make the implementation available for people to examine and change.&lt;/p&gt;

&lt;p&gt;And sometimes the experiment doesn't need to change the world. Sometimes it can just be an unnecessarily complicated joke that taught me something about building and deploying an AI application.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I have a few improvements in mind, though I'm not pretending all of them are urgent matters of public administration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Improve the model's reliability.&lt;/strong&gt; The current validation catches specific classes of invalid output, but it cannot guarantee that every interpretation is correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve case persistence.&lt;/strong&gt; The current deployment clears cases on restart. A more complete version would need persistent storage and a clearer data-retention policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the hosted demo more informative.&lt;/strong&gt; The sample-based setup demonstrates the application logic, but visitors cannot submit arbitrary input to a live model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the offices against more real inputs.&lt;/strong&gt; The existing tests verify application behaviour with mocked model responses. More systematic evaluation of actual Gemma outputs would help identify where the model still fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the unnecessary parts unnecessary.&lt;/strong&gt; The Sloth Officer and the hidden secrets are part of the point. I would rather keep the application's personality than turn it into a generic dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have not benchmarked DUA against doing these tasks manually, so I will not claim that it saves a particular percentage of time.&lt;/p&gt;

&lt;p&gt;On the built-in to-do example, the Bureau turns five tasks into one real deadline and two permits to ignore.&lt;/p&gt;

&lt;p&gt;The sample document is 84 words long and receives a reading-ease score of 40, which the Commission classifies as difficult. It identifies two deadlines, one action, two requirements, and four unclear points, each accompanied by a quotation that passes the source check.&lt;/p&gt;

&lt;p&gt;For an 84-word notice, the time saved may be negligible. A longer document might be a different story, but I have not tested that.&lt;/p&gt;

&lt;p&gt;I could try to turn those numbers into a productivity claim. I could also create a sixth office to investigate the matter, issue a report, and leave it unread.&lt;/p&gt;

&lt;p&gt;The second option feels more faithful to the project.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gemma 4 E4B is the model behind the five offices. It runs locally through Ollama, returns structured JSON, and provides the content that Python subsequently validates. The application uses code-side checks to reject specific kinds of invalid output rather than treating every model response as trustworthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Render&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The public Django application is deployed on Render through a Blueprint. The hosted service runs under Gunicorn, collects static files, applies migrations, and serves the sample-based demo using &lt;code&gt;DEMO_MODE=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keeping inference out of the hosted deployment avoids the additional resources needed to run Gemma on the server. Render makes the application publicly accessible, while the full local version remains available to anyone willing to install Ollama and download the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of GitHub Copilot (GitHub Actions)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The GitHub Actions workflow runs linting, Django's system check, the 244-test suite, and a production &lt;code&gt;collectstatic&lt;/code&gt; build step on every push.&lt;/p&gt;

&lt;p&gt;The tests use a mocked model, so they check how the application handles different outputs without requiring live inference for every test.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try the hosted demo:&lt;/strong&gt; &lt;a href="https://dua-demo.onrender.com" rel="noopener noreferrer"&gt;https://dua-demo.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the code:&lt;/strong&gt; &lt;a href="https://github.com/arkia09/DUA" rel="noopener noreferrer"&gt;https://github.com/arkia09/DUA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Officer S. Loth is still employed.&lt;/p&gt;

&lt;p&gt;Nobody has filed the paperwork to replace him.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I fine-tuned Qwen3.6 on Tinker to read my neighbours’ khata: 98% credit/payment accuracy for $0.38</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Mon, 19 Oct 2026 04:43:52 +0000</pubDate>
      <link>https://dev.to/fever_dreams/ledgerly-i-fine-tuned-a-model-to-read-a-shopkeepers-handwritten-khata-and-it-beat-gemma-on-5-2j4</link>
      <guid>https://dev.to/fever_dreams/ledgerly-i-fine-tuned-a-model-to-read-a-shopkeepers-handwritten-khata-and-it-beat-gemma-on-5-2j4</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;My neighbours run a grocery shop. Like thousands of small shops, they sell a lot on credit and keep track in a &lt;em&gt;khata&lt;/em&gt;: a paper ledger where every row is a customer, a date, an item, and either &lt;strong&gt;udhar&lt;/strong&gt; (the customer took goods on credit) or &lt;strong&gt;jama&lt;/strong&gt; (they paid something back).&lt;br&gt;
It works until you need an answer. Who owes what? Who hasn't paid in over a month? The answer is somewhere in the pages, and finding it means turning them over by hand.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Ledgerly&lt;/strong&gt;. You photograph a page. A vision model reads every row. You get a review screen with your photo beside the extracted rows, fix whatever it got wrong, confirm, and the app keeps running balances per customer, with an aging report: who is over 30 days.&lt;/p&gt;

&lt;p&gt;The part I care about most is the loop. Every page you confirm carries your corrections, and those corrections are the training data for the next fine-tune (&lt;code&gt;python ft/train.py --from-db&lt;/code&gt;). Using the app produces training data.&lt;/p&gt;

&lt;p&gt;It reads Hindi (Devanagari), English, and the mixed pages that real ledgers actually are.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

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

&lt;p&gt;The demo covers gemma running locally and extracting entries from a handwritten ledger page and other functionalities of Ledgerly web app.&lt;/p&gt;

&lt;h3&gt;
  
  
  What my neighbours said
&lt;/h3&gt;

&lt;p&gt;I asked my neighbours to try it. They found the screens easy to understand almost immediately: the photo next to the rows, the big buttons, the "owes money" and "over 30 days" filters. Their own khata is laid out differently from the pages I trained on, so they couldn't run their book through it yet. They told me they would definitely use it if I rolled it out as an app.&lt;/p&gt;

&lt;p&gt;That told me what to build next. The model has to learn &lt;em&gt;their&lt;/em&gt; page, and that is what fine-tuning is for: photograph a few pages, label them, and train again for about forty cents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The side hustle
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Who would pay:&lt;/strong&gt; small shops that sell on credit and still run their books on paper, like my neighbours. They don't need another accounting suite. They need to stop turning pages to find out who owes what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd sell:&lt;/strong&gt; the model and the pipeline are open and stay free. I'd charge for the convenience: a hosted version where the shopkeeper photographs a page and the ledger is ready, plus the part that matters most, &lt;strong&gt;teaching the model that shop's own page layout&lt;/strong&gt;. My neighbours' khata looks different from mine, so a one-time setup is exactly what they need: a few photographed and labelled pages, then a fine-tune. The compute for my 9-page run cost about forty cents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt;: I asked my neighbours what would feel fair for an app like this, and they said ₹100 to ₹150 a month. So I'd offer a free tier of about 20 pages a month, so a shop can try it on its own khata, then roughly ₹120-₹130 a month per shop. A one-time setup would teach the model that shop's own page layout. That's a few rupees a day, far less than the hours lost to the paper book, and in line with what small shops already pay for billing apps.&lt;/p&gt;

&lt;p&gt;The price comes from one shop's answer and the idea hasn't been tested beyond that, but they told me they would use it.&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/arkia09" rel="noopener noreferrer"&gt;
        arkia09
      &lt;/a&gt; / &lt;a href="https://github.com/arkia09/Ledgerly" rel="noopener noreferrer"&gt;
        Ledgerly
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Ledgerly&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Photograph a page of a paper credit ledger. A local Gemma model (via Ollama)
reads it, you correct mistakes, and the app reports who owes what and for how long.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Layout&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;extraction/&lt;/code&gt;   Step 1: model prompt, schema, single/batch runners, scoring&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backend/&lt;/code&gt;      Step 2: Django API (SQLite, no login)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frontend/&lt;/code&gt;     Step 3: React + TypeScript app (Vite)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;finetune/&lt;/code&gt;     Optional: fine-tune a vision model on Tinker and compare with the baseline (see its README)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data/pages/&lt;/code&gt;         test photos (gitignored: may contain customer data)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data/ground_truth/&lt;/code&gt;  your hand-written correct answers, one JSON per photo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data/outputs/&lt;/code&gt;       model outputs per run (gitignored)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup (Windows PowerShell)&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;python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
ollama pull gemma4:e4b
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Step 1 workflow&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Put photos in &lt;code&gt;data/pages/&lt;/code&gt; (page01.jpg, page02.jpg, ...).&lt;/li&gt;
&lt;li&gt;For each, copy &lt;code&gt;data/ground_truth/_template.json&lt;/code&gt; to &lt;code&gt;page01.json&lt;/code&gt; and fill in the true rows
Set "page_type" to neat, messy or mixed.&lt;/li&gt;
&lt;li&gt;Edit the layout line in &lt;code&gt;extraction/prompt.txt&lt;/code&gt; to match your ledger.&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/arkia09/Ledgerly" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Django + SQLite backend, React + TypeScript frontend, and an &lt;code&gt;extraction/ft/&lt;/code&gt; folder that holds the whole fine-tuning pipeline: &lt;code&gt;dataset.py&lt;/code&gt;, &lt;code&gt;train.py&lt;/code&gt;, &lt;code&gt;sampler.py&lt;/code&gt;, &lt;code&gt;evaluate.py&lt;/code&gt;, &lt;code&gt;compare.py&lt;/code&gt;.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;photo ──▶ preprocess
      ──▶ vision model ──▶ JSON { rows: [ {date, name, item, amount, type, struck_out} ] }
      ──▶ pydantic schema check
      ──▶ review screen (you fix rows) ──▶ confirm ──▶ ledger + aging report
                                                │
                    corrections ◀───────────────┘  (next training run, --from-db)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row has exactly six fields. &lt;code&gt;type&lt;/code&gt; is &lt;code&gt;credit&lt;/code&gt; (udhar) or &lt;code&gt;payment&lt;/code&gt; (jama), and &lt;code&gt;struck_out&lt;/code&gt; is true when the shopkeeper has crossed a row out, so it is stored but not counted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15/10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ramesh Kumar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Atta (5 kg)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"struck_out"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: the baseline, with a local Gemma
&lt;/h3&gt;

&lt;p&gt;The first version ran &lt;strong&gt;Gemma 4 e4b locally through Ollama&lt;/strong&gt;. Nothing leaves the computer, which matters for a book full of customers' names and debts. It works, but it takes about &lt;strong&gt;159 seconds per page&lt;/strong&gt; on my CPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: the data (the boring part that decides everything)
&lt;/h3&gt;

&lt;p&gt;I labelled 14 pages by hand (took me 2 hours), one JSON of ground truth per photo. Then I split them so the model is judged on pages it has never seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Held out (5 pages, 50 rows):&lt;/strong&gt; page02, page04, page06 (my original neat, messy and mixed pages) plus page09 and page10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trained on (9 pages):&lt;/strong&gt; the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I picked page09 and page10 as test pages on purpose. Several of my new pages repeat each other's customers and amounts (page 8 and page 14 are nearly identical). If a test page has a twin in training, the score is inflated. Those two had no twins.&lt;/p&gt;

&lt;p&gt;Nine pages is tiny, so &lt;code&gt;dataset.py&lt;/code&gt; adds three augmented copies of each page (small rotation, brightness, contrast and crop). That makes &lt;strong&gt;36 training examples, 79,764 tokens per epoch&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: fine-tuning on Tinker
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Choosing the model
&lt;/h4&gt;

&lt;p&gt;The input is a photograph, so I needed a &lt;strong&gt;vision&lt;/strong&gt; model. My first choice, Qwen3-VL-30B-A3B, died on the first real run with &lt;code&gt;400: base_model ... is not supported&lt;/code&gt;. The public docs still listed it. So I asked the server what my key could use (&lt;code&gt;ServiceClient().get_server_capabilities()&lt;/code&gt;), and the list had moved on to Qwen3.5 and Qwen3.6, plus Nemotron and others.&lt;/p&gt;

&lt;p&gt;I picked &lt;strong&gt;Qwen3.6-35B-A3B&lt;/strong&gt;: a mixture-of-experts model with about 3B active parameters, which keeps training and sampling cheap, in the same size class as the model I'd planned. A free &lt;code&gt;--dry-run&lt;/code&gt; rendered every example with the real tokenizer and image processor before I spent anything, and the renderer it chose (&lt;code&gt;qwen3_5&lt;/code&gt;) matched what &lt;code&gt;tinker_cookbook&lt;/code&gt; recommends for that model.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://thinkingmachines.ai/blog/tinker-general-availability/" rel="noopener noreferrer"&gt;Tinker&lt;/a&gt; to LoRA fine-tune it with the settings in &lt;code&gt;train.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Method&lt;/td&gt;
&lt;td&gt;LoRA, rank 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epochs / batch size&lt;/td&gt;
&lt;td&gt;4 / 4, so 36 steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning rate&lt;/td&gt;
&lt;td&gt;1e-4, decaying to 1e-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renderer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;qwen3_5&lt;/code&gt; (matches &lt;code&gt;get_recommended_renderer_name&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training cost&lt;/td&gt;
&lt;td&gt;about $0.38 of credit ($10.00 to $9.62)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every example is a &lt;code&gt;(photo, correct JSON)&lt;/code&gt; pair.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;epoch 1/4 step 1/36  loss/token 0.1466
epoch 1/4 step 9/36  loss/token 0.0185
epoch 2/4 step 18/36 loss/token 0.0050
epoch 3/4 step 27/36 loss/token 0.0013
epoch 4/4 step 36/36 loss/token 0.0008
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;I'm not treating that curve as a result. It's loss on training pages the model saw four times each, so it says the run was healthy, not that the model is good.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: the evaluation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ft/evaluate.py&lt;/code&gt; runs the untouched base model and the fine-tuned model on the five held-out pages and scores both with my own &lt;code&gt;score.py&lt;/code&gt;. Each true row has to be matched to a predicted row, and then name, amount, date, type and crossed-out flag are checked. A row counts as "full" only when all five are right.&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%2Fnylo7etp8rwkadu90kcs.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%2Fnylo7etp8rwkadu90kcs.png" alt="ft/evaluate.py printing both score tables" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The base model scored 0%.&lt;/strong&gt; I want to be precise about why. It did not read the pages badly. On all five pages it failed to produce valid JSON for my schema (wrong shape, no JSON object at all, or text after the JSON). &lt;code&gt;score.py&lt;/code&gt; can only score what it can parse, so every page became "found 0 rows". That number measures instruction-following on one schema, not handwriting, so I'm not using it as my "before". &lt;strong&gt;Gemma is my baseline&lt;/strong&gt;, because it produces valid output.&lt;/p&gt;

&lt;p&gt;Fine-tuned Qwen against local Gemma, 50 rows, same pages, same checker:&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;Gemma 4 e4b (local)&lt;/th&gt;
&lt;th&gt;Qwen3.6 fine-tuned (Tinker)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full row correct&lt;/td&gt;
&lt;td&gt;41/50 (82%)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;44/50 (88%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;46/50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;45/50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount&lt;/td&gt;
&lt;td&gt;48/50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49/50&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;47/50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49/50&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type (credit vs payment)&lt;/td&gt;
&lt;td&gt;44/50 (88%)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49/50 (98%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crossed out&lt;/td&gt;
&lt;td&gt;47/50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48/50&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra rows invented&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seconds per page&lt;/td&gt;
&lt;td&gt;159 s (laptop CPU)&lt;/td&gt;
&lt;td&gt;12 s (Tinker cloud GPU)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fully right rows by kind of page (this is the part I'd look at before trusting any single percentage):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page type&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Gemma 4 e4b (local)&lt;/th&gt;
&lt;th&gt;Qwen3.6 fine-tuned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Neat (page02, page09, page10)&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;26 (87%)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28 (93%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messy (page04)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7 (70%)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed Hindi/English (page06)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8 (80%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6 (60%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What stands out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Credit versus payment went from 88% to 98%.&lt;/strong&gt; In a ledger that is the field that matters most: mixing up udhar and jama flips the sign on someone's balance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three more rows fully right out of 50&lt;/strong&gt; (82% to 88%), with dates, amounts and crossed-out flags all a little better and no invented rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12 seconds a page instead of 159.&lt;/strong&gt; That compares a cloud GPU with my laptop CPU, so read it as "hosted versus on my machine", not as a model-size result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it's level or behind:&lt;/strong&gt; names (45 against Gemma's 46) and my one mixed Hindi/English page (6 of 10 rows against 8). On the neat and messy pages it got 28 of 30 and 10 of 10 rows fully right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With 50 test rows, a one or two row gap is noise, so I trust the type and speed results most.&lt;/p&gt;

&lt;p&gt;Two details from walking through every row in &lt;code&gt;compare.html&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One label was my mistake.&lt;/strong&gt; On page06, row 7, my ground-truth JSON said the row was crossed out, but I never crossed it out on the paper. Both models said "not crossed out", so both were marked wrong for being right. I corrected that one label after seeing the results and rescored both models, so the numbers above include the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One real miss:&lt;/strong&gt; the fine-tuned model wrote "Pooja" in Latin letters where the ledger has the name in Devanagari. It's the same person, but the tool is meant to copy what's written, so I left it counted as wrong. Several of the other name misses were rows Gemma read correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: seeing the mistakes, not just the percentages
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ft/compare.py&lt;/code&gt; builds one HTML page: my photo, my ground truth, and every model side by side, with wrong fields in red and the correct value underneath. I used it to find that the mixed page was where the fine-tuned model lost rows.&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%2F795qcd00rz6blnapmc84.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%2F795qcd00rz6blnapmc84.png" alt="compare.html" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Using it in the app
&lt;/h3&gt;

&lt;p&gt;One environment variable decides who reads the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LEDGERLY_EXTRACTOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tinker"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# default is the local Gemma via Ollama&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default keeps every photo on the computer. With &lt;code&gt;tinker&lt;/code&gt;, each photo is sent to Tinker's cloud to be read by the fine-tuned model, and the README says so. The API key lives in an environment variable and nowhere in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong on the way
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I almost trained on three pages.&lt;/strong&gt; I had put all eight new pages into the held-out list. That left the training set exactly where it started. Reading the dry-run's "36 examples" line is what caught the mistake.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;My new photos were &lt;code&gt;.jpeg&lt;/code&gt;, and the pipeline looked for &lt;code&gt;.jpg&lt;/code&gt;.&lt;/strong&gt; They were skipped without a single warning. Renaming fixed it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Label quality matters as much as quantity.&lt;/strong&gt; Some of the mistakes I found in &lt;code&gt;compare.html&lt;/code&gt; were mine, not the model's: a "crossed out" label on a row I never crossed out. A model can only be as consistent as the labels it is trained and judged on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure the baseline fairly.&lt;/strong&gt; Base Qwen's 0% looked dramatic and meant "can't follow my JSON format". Gemma was the honest comparison.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Look at the rows, not just the percentages.&lt;/strong&gt; The scoreboard said the fine-tuned model was a little better. The side-by-side page showed where: credit versus payment and the neat and messy pages, not the mixed one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dry-run first.&lt;/strong&gt; The whole fine-tune cost about forty cents because I rendered everything for free before spending any credit.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learn the neighbours' khata.&lt;/strong&gt; Label a few pages in their layout and run the same pipeline. Every page they confirm in the app becomes training data (&lt;code&gt;--from-db&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More writers.&lt;/strong&gt; My own handwriting is in all 14 pages. Pages from other hands will show how far the model carries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the adapter&lt;/strong&gt; on Hugging Face (Tinker can push a checkpoint straight there), so anyone can run it on their own hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package it as an app&lt;/strong&gt; so a shopkeeper can use it without a laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The review screen is part of the design: the model proposes the rows, the shopkeeper confirms them, and nothing reaches the ledger unchecked.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It stays on the shopkeeper's computer.&lt;/strong&gt; A khata is a list of who owes whom. With open weights the default path never uploads a photo, and the cloud path is opt-in and said out loud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could change the model and measure it.&lt;/strong&gt; Tinker let me fine-tune open weights for about 38 cents, and I could run that against an open baseline on the same pages with the same scorer. Swapping the base model is one variable (&lt;code&gt;TINKER_BASE_MODEL&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could see why it was wrong.&lt;/strong&gt; The comparison page shows each wrong field next to the right value, so I could tell a reading error from a spelling variant instead of trusting one score.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The result is reproducible.&lt;/strong&gt; Open weights, my labelled pages, my scorer and the saved outputs are all in the repo, so anyone can rerun the comparison.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Best Use of Tinker:&lt;/strong&gt; LoRA fine-tune of Qwen3.6-35B-A3B on 36 augmented examples, scored on 5 held-out pages against a local baseline, for about $0.38.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 e4b runs locally through Ollama as the private default and as the baseline the fine-tuned model has to beat.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with Claude and tea ☕.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Who Do You Report a Pothole To? I Built Civic Lens to Answer That</title>
      <dc:creator>Simran Kaur</dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:56:32 +0000</pubDate>
      <link>https://dev.to/arkia09/who-do-you-report-a-pothole-to-i-built-civic-lens-to-answer-that-1nlg</link>
      <guid>https://dev.to/arkia09/who-do-you-report-a-pothole-to-i-built-civic-lens-to-answer-that-1nlg</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;Living in India comes with a lot of infrastructure problems, and somewhere along the way we stopped seeing them. A garbage pile at the corner, a broken footpath, a dead streetlight. Nobody fixes them, and honestly we don't either. We walk past and carry on with the day as if everything is fine.&lt;/p&gt;

&lt;p&gt;Even people who do notice usually don't know who is responsible or where to report it. I didn't, until I built this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Civic Lens&lt;/strong&gt; is a web app that makes you actually look. You type your area and get a short quest with three categories: safety and infrastructure, cleanliness and environment, accessibility and public space. For each one you go outside, photograph a real problem, and write how you'd fix it. Then you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a score for the problem and for your proposed fix&lt;/li&gt;
&lt;li&gt;the kind of authority responsible for it&lt;/li&gt;
&lt;li&gt;a 4-step action plan&lt;/li&gt;
&lt;li&gt;a complaint draft, written for you, ready to copy&lt;/li&gt;
&lt;li&gt;a link to an official government reporting page, when one can be found&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do all three and you get "Grass Touched 🌱". The point isn't the points. It's learning who is responsible for the everyday problems around you, and having a complaint ready to send.&lt;/p&gt;

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

&lt;p&gt;I've added a demo video in project's readme. Please check it out if you get a chance :)&lt;br&gt;
Live Site: &lt;a href="https://civiclens-web-v1aa.onrender.com/" rel="noopener noreferrer"&gt;https://civiclens-web-v1aa.onrender.com/&lt;/a&gt;&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%2F1lmkm3lwi00xa8hayvno.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%2F1lmkm3lwi00xa8hayvno.png" alt=" " width="800" height="377"&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%2Fsytdmyrwjrez4cb43cb4.jpeg" 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%2Fsytdmyrwjrez4cb43cb4.jpeg" alt=" " width="720" height="1520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Taking it outside:&lt;/strong&gt; I walked around my area and tried it for real. It felt awkward. Standing on the road photographing a garbage pile or a broken stretch of pavement from different angles, with people walking past, is not something I'm used to, and I caught myself wanting to hurry up and move on.&lt;/p&gt;

&lt;p&gt;I took 5 pics. For the demo video I uploaded them with the "Choose from gallery" button, so the in-app camera flow isn't in the recording.&lt;/p&gt;

&lt;p&gt;What I saw when I ran them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scores:&lt;/strong&gt; they felt fair. My proposed fixes were short and not very detailed, and the scores reflected that, so the grader wasn't just handing out high marks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting channel:&lt;/strong&gt; I only tried "Find reporting channel" once, on an infrastructure issue. It returned a generic Delhi government link that was probably not the right place for road complaints, and it still gave me the +2 bonus. This is the weakest part of the app right now, and I explain why below.&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/arkia09" rel="noopener noreferrer"&gt;
        arkia09
      &lt;/a&gt; / &lt;a href="https://github.com/arkia09/CivicLens" rel="noopener noreferrer"&gt;
        CivicLens
      &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/arkia09/CivicLens/frontend/public/mascot.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Farkia09%2FCivicLens%2FHEAD%2Ffrontend%2Fpublic%2Fmascot.webp" alt="Civic Lens mascot" width="120"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Civic Lens&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  Photograph three real problems in your neighbourhood, propose a fix, and get a score
  the responsible authority and a ready-to-send complaint
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/arkia09/CivicLens/LIVE_URL" rel="noopener noreferrer"&gt;Live app&lt;/a&gt; ·
  &lt;a href="https://github.com/arkia09/CivicLens/DEV_POST_URL" rel="noopener noreferrer"&gt;DEV post&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05" rel="nofollow"&gt;Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass&lt;/a&gt;.
The backend is on a free Render plan and sleeps when idle, so the first request can be slow.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo Video&lt;/h2&gt;

&lt;/div&gt;

  
    
    &lt;span class="m-1"&gt;Video.Project.5.1.1.1.1.mp4&lt;/span&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;Gemma writes a short quest for your area, with one hint for each of three categories.&lt;/li&gt;
&lt;li&gt;Upload a photo and your proposed fix. Gemma checks the photo, scores the problem and your fix,
names the kind of authority responsible, and drafts a complaint.&lt;/li&gt;
&lt;li&gt;SerpApi looks for an official government reporting page (&lt;code&gt;.gov.in&lt;/code&gt; / &lt;code&gt;.nic.in&lt;/code&gt;) for a +2 bonus.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tech stack&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI, Pydantic, httpx, Pillow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React, TypeScript, Vite, Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; Gemma via the &lt;code&gt;google-genai&lt;/code&gt; SDK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search:&lt;/strong&gt; SerpApi&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Render&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Project structure&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;civiclens/
├──&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/arkia09/CivicLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
This is how my project's structure looks like:&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;civiclens/
├── backend/
│   ├── main.py        # FastAPI routes: /api/quest, /api/analyze, /api/find-channel
│   ├── gemma.py       # everything that talks to Gemma
│   ├── search.py      # SerpApi lookup, ranking, cache, daily cap
│   ├── prompts.py     # all prompt text, separate from logic
│   ├── models.py      # Pydantic models, the API contract
│   ├── utils.py       # image checks, duplicate detection, score math
│   ├── mock_data.py   # canned data for frontend work
│   └── tests/
├── frontend/
│   └── src/
│       ├── App.tsx, api.ts, points.ts, types.ts
│       └── components/   # SlotCard, ResultView, ChannelFinder, Completion, Mascot
└── render.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Stack: FastAPI, React with Tailwind, Gemma for analysis, SerpApi for the reporting link, Render for hosting. I spent most of my time on the backend. I'll be upfront that Claude built almost all of the UI, because I had very little time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Gemma is used
&lt;/h3&gt;

&lt;p&gt;I call Gemma through Google's API with the &lt;code&gt;google-genai&lt;/code&gt; SDK. There are two jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The quest&lt;/strong&gt; is text only. Gemma writes a short briefing and one hint per category for the area you typed. If it fails, the app falls back to a static template, so a quest never fails to load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Photo analysis&lt;/strong&gt; is the main one. When you submit a photo and your fix, the backend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks the file (JPG or PNG, size limit, magic bytes so a renamed file can't pass) and hashes it to catch reused photos.&lt;/li&gt;
&lt;li&gt;Fixes phone rotation and shrinks it to 1280px, since phone photos are huge.&lt;/li&gt;
&lt;li&gt;Sends the image plus a prompt to Gemma, with a system instruction at low temperature (0.2).&lt;/li&gt;
&lt;li&gt;Gets back JSON, parses it, and validates it with Pydantic. If the JSON is broken, it retries once.&lt;/li&gt;
&lt;li&gt;Checks &lt;code&gt;photo_check&lt;/code&gt; first: is this a real outdoor photo, does it show a civic problem, and does it match this category? If not, the user gets a friendly rejection instead of a score.&lt;/li&gt;
&lt;li&gt;Computes the total score in the backend. The model gives two numbers from 0 to 10 and never adds anything up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the primary model errors out (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;), the backend retries with a fallback (&lt;code&gt;gemma-4-31b-it&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Because users type text that goes into a prompt, I treat it as data. The user's text is wrapped in tags, the system prompt says to never follow instructions inside them, and I strip any attempt to close those tags. A "give me 10/10" solution gets a zero.&lt;/p&gt;

&lt;p&gt;Right now the three category names are hardcoded, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SLOT_TITLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safety_infrastructure&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;Safety &amp;amp; infrastructure&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;cleanliness_environment&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;Cleanliness &amp;amp; environment&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;accessibility_public_space&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;Accessibility &amp;amp; public space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma already writes the hints for each area, but not the categories. Having Gemma pick different categories on every Start over is next on my list.&lt;/p&gt;

&lt;h3&gt;
  
  
  SerpApi
&lt;/h3&gt;

&lt;p&gt;SerpApi searches Google for the official page where a complaint can be filed, like &lt;code&gt;[area] [authority] [issue] complaint grievance portal&lt;/code&gt;. I only accept results on &lt;code&gt;.gov.in&lt;/code&gt; or &lt;code&gt;.nic.in&lt;/code&gt; that also mention words like "grievance" or "portal". If nothing qualifies, the app says so and gives no bonus. A found page is worth +2 points, so the app rewards knowing where to report.&lt;/p&gt;

&lt;p&gt;I had never heard of SerpApi before this challenge, so I spent a lot of time in its documentation. It helped me fix my errors and get it into the app.&lt;/p&gt;

&lt;p&gt;Limits right now, and I'm working on them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sometimes the search fails and my backend returns a 503. Pressing "Try again" usually works.&lt;/li&gt;
&lt;li&gt;My filter only checks that a result is on an official domain (&lt;code&gt;.gov.in&lt;/code&gt; or &lt;code&gt;.nic.in&lt;/code&gt;) and mentions words like "grievance" or "portal". It does not check that the page belongs to the right department. When I tested an infrastructure issue, it returned a generic Delhi government link that was probably not the right channel for road complaints, and it still gave me the +2 bonus. In an earlier test it found a proper MCD page, so it works sometimes, but I can't promise it's right. The bonus rewards finding an official page, not verifying it.&lt;/li&gt;
&lt;li&gt;There's a daily cap of 50 searches to protect my quota, and an in-memory cache. Both reset when the server restarts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Render, and the hour I'll never get back
&lt;/h3&gt;

&lt;p&gt;I hadn't used Render before, so I had ChatGPT walk me through the dashboard. It told me I didn't need to import my &lt;code&gt;.env&lt;/code&gt;, so I added the variables by hand. And I misspelled &lt;code&gt;CORS_ORIGINS&lt;/code&gt; as &lt;code&gt;CORS_ORIGIN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My deployed site then threw this in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access to fetch at 'https://civiclens-xyhi.onrender.com/api/quest' from origin
'https://civiclens-web-v1aa.onrender.com' has been blocked by CORS policy...
No 'Access-Control-Allow-Origin' header is present on the requested resource.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I spent about an hour suspecting my backend. It was a spelling mistake, plus a trailing slash on the API URL that produced &lt;code&gt;//api/quest&lt;/code&gt;. The code ignores a variable with the wrong name and falls back to &lt;code&gt;localhost&lt;/code&gt;, so nothing complained. Next time I check the spelling first.&lt;/p&gt;

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

&lt;p&gt;Gemma has open weights. To be honest about my setup: I use it through Google's hosted API, so it isn't running locally or offline. What open weights gave me is a way out.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; The scoring and complaint writing come from prompts I wrote and control (&lt;code&gt;prompts.py&lt;/code&gt;). If a better open model shows up, I change a model name, not the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallbacks I choose.&lt;/strong&gt; I use two different Gemma models as primary and fallback, and I can add more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A path to privacy.&lt;/strong&gt; People are photographing their own streets. With open weights, I could later run this on a server I control so those photos don't go to a third party. That's a future step, not what's live today.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Gemma picks different categories every time you start over, so quests aren't the same each time.&lt;/li&gt;
&lt;li&gt;A leaderboard. Today your points live only in your own browser (they survive a refresh, but not a different device), so anyone can edit them. A real leaderboard needs a database and some way to trust the scores.&lt;/li&gt;
&lt;li&gt;Better search accuracy for the official reporting page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Known limits: the app can't prove you went outside, only that you sent a convincing photo. And the free backend resets its cache and duplicate-photo list on restart.&lt;/p&gt;

&lt;p&gt;There's still a long way to go, but it works, and I now know who to call about the garbage pile on my street.&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; quest writing and the photo analysis, scoring and complaint drafting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; the API and the static frontend both run on Render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of SerpApi:&lt;/strong&gt; finding the official government reporting page, which drives the bonus points.&lt;/li&gt;
&lt;/ul&gt;

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