<?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: Rahul Tapase</title>
    <description>The latest articles on DEV Community by Rahul Tapase (@rahultapase).</description>
    <link>https://dev.to/rahultapase</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%2F4154030%2F3b69fdee-fbb5-46c2-8f1a-ca4ee7a58cdf.png</url>
      <title>DEV Community: Rahul Tapase</title>
      <link>https://dev.to/rahultapase</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rahultapase"/>
    <language>en</language>
    <item>
      <title>PrepMate: Practice Interviews While Gemma 4 Does the Prep Work</title>
      <dc:creator>Rahul Tapase</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:09:41 +0000</pubDate>
      <link>https://dev.to/rahultapase/prepmate-practice-interviews-while-gemma-4-does-the-prep-work-3icj</link>
      <guid>https://dev.to/rahultapase/prepmate-practice-interviews-while-gemma-4-does-the-prep-work-3icj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;PrepMate&lt;/strong&gt; is a mock interviewer that runs on your own laptop. You drop in your resume PDF, paste the job description, pick Technical, HR or Managerial, and then you just talk. Gemma 4 reads your background, asks you tailored questions out loud, listens to your spoken answers, and when you end the session it hands you a scorecard: a communication score and a technical (or behavioral) score for every answer, a written comment on each one, an overall score, and a short list of things to work on.&lt;/p&gt;

&lt;p&gt;Interview prep is the task everyone puts off. Doing it properly means hunting for question lists that match your role, rehearsing answers alone in your head with no idea whether they're any good, or convincing a friend to play interviewer for an hour and then give you honest feedback. PrepMate does all three jobs. The only effort left for you is showing up and speaking.&lt;/p&gt;

&lt;p&gt;The lazy parts it handles for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No question hunting.&lt;/strong&gt; Questions are generated from your resume, target role, company and job description, so a frontend intern and a backend engineer get different interviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No typing.&lt;/strong&gt; Questions are read aloud and your answers are captured by voice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No self-grading.&lt;/strong&gt; Every answer is scored and commented on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tuning.&lt;/strong&gt; After five completed sessions of the same type, the questions get harder on their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No bookkeeping.&lt;/strong&gt; Every session lands on a dashboard with search, filters and the full feedback, so you can see whether you're improving without keeping notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm a student heading into interview season, so I'm the first lazy person this was built for.&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%2Fuxwdi1fhl0kltvxa6ejf.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%2Fuxwdi1fhl0kltvxa6ejf.png" alt="PrepMate home page with the Start Practicing button and a preview of an AI interview session" width="800" height="385"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The home page. One button and you're in.&lt;/em&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%2F9yglve9nejsgrlss7hgg.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%2F9yglve9nejsgrlss7hgg.png" alt="Configure Session form with fields for name, job role, company, experience, interview type, job description, and resume content with an Upload PDF button" width="800" height="385"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Setting up a session: fill in the role, paste the job description, and upload your resume as a PDF. That's all the typing you'll do.&lt;/em&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/rahultapase" rel="noopener noreferrer"&gt;
        rahultapase
      &lt;/a&gt; / &lt;a href="https://github.com/rahultapase/Prep-mate" rel="noopener noreferrer"&gt;
        Prep-mate
      &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&gt;
  &lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🎯 PrepMate - AI Mock Interview Platform&lt;/h1&gt;
&lt;/div&gt;
  &lt;p&gt;Practice interviews with a local AI interviewer, answer by voice, and get scored feedback on every session.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/rahultapase/Prep-mate/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7b032738aeb4a8dd7ec78c4402e078ca8c196b0e528c117bbd78088c9a036988/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75653f7374796c653d666f722d7468652d6261646765" alt="License"&gt;&lt;/a&gt;
&lt;a href="https://react.dev/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4f499c63d688ea7aa37ea48206203cdc5c8664f67ecb9a15a5f01442b3aa4acb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f52656163742d31382d3631646166623f7374796c653d666f722d7468652d6261646765266c6f676f3d7265616374" alt="React"&gt;&lt;/a&gt;
&lt;a href="https://www.typescriptlang.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4189e40a91ff500d22425ccba99f634ca9f4b8d7c12b88c211755b05b2a14e06/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d352d626c75653f7374796c653d666f722d7468652d6261646765266c6f676f3d74797065736372697074" alt="TypeScript"&gt;&lt;/a&gt;
&lt;a href="https://ollama.com/library/gemma4" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ae068bff6f40d1906cd1193316f519d22eb8f630cafc87604c5b96c5246f3456/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f47656d6d615f342d4f6c6c616d612d626c61636b3f7374796c653d666f722d7468652d6261646765" alt="Gemma 4"&gt;&lt;/a&gt;
&lt;a href="https://www.sqlite.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/08770f4623d633c7ddacb038796b001460946e249edf381fb90e3ba9c46ab5b4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53514c6974652d6c6f63616c2d3030334235373f7374796c653d666f722d7468652d6261646765266c6f676f3d73716c697465" alt="SQLite"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📖 About&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;PrepMate runs a full mock interview on your own machine. It reads your resume and the job description, asks tailored questions out loud, records your spoken answers, and then scores each answer with written feedback. The AI model (Gemma 4) runs locally through Ollama, and all data is stored in a local SQLite file.&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;AI interviewer&lt;/strong&gt; - Questions generated by Gemma 4 from your resume, target role, company and job description&lt;/li&gt;
&lt;li&gt;📄 &lt;strong&gt;PDF resume upload&lt;/strong&gt; - Text is extracted in the browser and can be edited before the interview; pasting text also works&lt;/li&gt;
&lt;li&gt;🎙️ &lt;strong&gt;Voice answers&lt;/strong&gt; - Questions are read aloud and answers are captured with speech-to-text&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Interview types&lt;/strong&gt; - Technical, HR and Managerial&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Scored feedback&lt;/strong&gt; - Per-question communication…&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/rahultapase/Prep-mate" 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;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (React + TypeScript + Vite)
   │  /api/*
   ▼
Express on 127.0.0.1:3001
   ├── auth  → SQLite users + sessions
   ├── data  → SQLite profiles, interviews, ratings
   └── ai    → Ollama at 127.0.0.1:11434 (gemma4:e2b)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server only listens on &lt;code&gt;127.0.0.1&lt;/code&gt;, and the resume PDF never leaves the browser: pdf.js extracts the text client-side, and only that text goes into the prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Talking to Gemma
&lt;/h3&gt;

&lt;p&gt;The whole AI layer is one call to Ollama's chat API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/chat`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// gemma4:e2b by default&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;think&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;// skip the reasoning trace, it only adds latency here&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt; &lt;span class="c1"&gt;// JSON schema for feedback&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;num_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;numCtx&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// 16384&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;Three settings did most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;think: false&lt;/code&gt;.&lt;/strong&gt; A list of interview questions doesn't need a reasoning trace, and skipping it cuts the wait noticeably on a laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;num_ctx: 16384&lt;/code&gt;.&lt;/strong&gt; A resume (up to 10k characters) plus a job description (up to 8k) plus the instructions doesn't fit in Ollama's default context window, and the prompt would get truncated. Raising it keeps the whole resume in view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-constrained feedback.&lt;/strong&gt; The dashboard reads exact fields, so the feedback has to be valid JSON every time. App sends a JSON schema as Ollama's &lt;code&gt;format&lt;/code&gt;, and Gemma's output is constrained to that shape. HR and Behavioral interviews get a behavioral score; Technical and Managerial get a technical score:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perQuestion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;softSkillsInterview&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;communicationScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scoreInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fluencyComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;behavioralComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;communicationScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scoreInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;technicalScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scoreInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fluencyComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;techComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Small model, clear rules
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gemma4:e2b&lt;/code&gt; is small enough to run on an ordinary laptop, so the prompts are explicit about format: one question per line, no headings, no labels, nothing else. A tolerant parser then keeps lines that end in a question mark, so a stray intro sentence from the model doesn't end up as "question 1".&lt;/p&gt;

&lt;h3&gt;
  
  
  Being honest about the hardware
&lt;/h3&gt;

&lt;p&gt;Before an interview starts, a system check calls &lt;code&gt;/api/health&lt;/code&gt;, which asks Ollama whether it's running and whether the model is installed. If either is missing, the Start button stays locked and the page shows the exact command to fix it (&lt;code&gt;ollama serve&lt;/code&gt; or &lt;code&gt;ollama pull gemma4:e2b&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Local generation is slower than a cloud API. On &lt;code&gt;gemma4:e2b&lt;/code&gt;, questions take roughly 20 to 60 seconds and feedback 10 to 30 seconds, depending on the machine. The app says so up front instead of pretending otherwise. For a week themed on sloths, a short wait in exchange for keeping everything on your own disk felt like a fair trade.&lt;/p&gt;

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

&lt;p&gt;A mock interview is personal data. Your resume carries your phone number, email, college and grades, and your answers include things like "tell me about a time you failed." The resume text, questions, answers and scores live in one SQLite file on your disk, and the API isn't reachable from outside your machine.&lt;/p&gt;

&lt;p&gt;Where the open approach worked better than the closed one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per interview.&lt;/strong&gt; Practice only works if you do it a lot. With a local model there's no quota or bill to ration, so a tenth mock interview costs the same as the first: nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No API key.&lt;/strong&gt; Setup is one &lt;code&gt;ollama pull&lt;/code&gt;, and nothing expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the model with one line.&lt;/strong&gt; &lt;code&gt;OLLAMA_MODEL=gemma4:e4b&lt;/code&gt; gives better questions and feedback if your machine has the memory for it. Nothing else changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control over the output.&lt;/strong&gt; Constraining Gemma to a JSON schema through Ollama made feedback dependable enough to build a dashboard on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest caveat: speech-to-text still uses the browser's Web Speech API, and Chrome sends that audio to Google's speech service. So PrepMate isn't fully offline yet. The AI, the data and your resume are local, but live transcription needs a connection. Moving transcription to a local Whisper model is the next step to close that gap.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 (&lt;code&gt;gemma4:e2b&lt;/code&gt;) runs locally through Ollama and generates every interview question, the follow-up rounds, and all scored feedback, with schema-constrained JSON output for the scorecards.
&amp;lt;!-- Only keep the line below if you embed or link a DevRelay agent session above --&amp;gt;
&amp;lt;!-- - &lt;strong&gt;Best Use of Entire:&lt;/strong&gt; the agent session behind this build is embedded above. --&amp;gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading. If you try it, I'd like to hear how it scored you.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Shopkeepers won't retype their udhaar khata. KhataLens reads it with Gemma 4, locally</title>
      <dc:creator>Rahul Tapase</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:23:05 +0000</pubDate>
      <link>https://dev.to/rahultapase/shopkeepers-wont-retype-their-udhaar-khata-khatalens-reads-it-with-gemma-4-locally-3lpi</link>
      <guid>https://dev.to/rahultapase/shopkeepers-wont-retype-their-udhaar-khata-khatalens-reads-it-with-gemma-4-locally-3lpi</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;Walk into almost any kirana shop and you'll find a notebook next to the till: the &lt;strong&gt;udhaar khata&lt;/strong&gt;. A regular customer takes five kilo atta today and pays on Saturday, so the shopkeeper writes it down: date, name, item, amount. When money comes back, he writes "jama".&lt;/p&gt;

&lt;p&gt;The notebook works until he needs a total. Knowing who owes what means re-adding pages of handwriting, with the same customer scattered across many pages. Then comes the harder part: asking a neighbour for money without it getting awkward. So dues slip.&lt;/p&gt;

&lt;p&gt;Free ledger apps like Khatabook and OkCredit exist, but they ask the shopkeeper to type every entry into a phone, on top of the notebook he already keeps. &lt;strong&gt;KhataLens&lt;/strong&gt; starts from the notebook instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Photograph the pages.&lt;/strong&gt; One page or a whole book. Gemma 4 reads every handwritten line into an editable ledger that sits next to the photo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check it.&lt;/strong&gt; Fix any misread in place. Corrected rows are tagged, and balances update as you type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get the hisaab.&lt;/strong&gt; The same customer across pages adds up into one balance (udhaar minus jama), sorted by who owes most, with the total to collect always visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send a polite reminder.&lt;/strong&gt; Gemma drafts a short, respectful Hinglish message with the customer's name, the exact ₹ amount, the shop name and, optionally, the shop's UPI ID. The shopkeeper copies it or opens WhatsApp, picks the contact and taps send himself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export.&lt;/strong&gt; Download the ledger and the balances as a CSV for Excel.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything runs on the shop's own computer. Gemma 4 E2B runs locally in Ollama, so the photos, names and amounts never leave it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay, and for what.&lt;/strong&gt; The customer is the shopkeeper. He wouldn't be paying for a ledger, because those are free. He'd be paying to not change his habit: he keeps writing in the notebook, and once a week he photographs the new pages and gets every balance plus a ready-to-send reminder for each customer. If the reminders bring back one forgotten ₹500 a month, a ₹99 monthly fee pays for itself. That price only works because reading a page costs nothing: there's no per-image API bill that grows with every page he photographs.&lt;/p&gt;

&lt;p&gt;To be upfront: no shopkeeper has used it yet. Everything below was tested on three khata pages I handwrote myself, with made-up names.&lt;/p&gt;

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

&lt;p&gt;KhataLens needs Ollama on the same machine, by design, so there's no hosted demo. Setup is a model pull and two npm commands, all in the README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Three handwritten pages, one ledger.&lt;/strong&gt; The photo stays next to the table, and every row is tagged with the page it came from (P1, P2, P3), so the shopkeeper can always go back to the paper:&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%2Fluvigsc7y9uj7vxemvti.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%2Fluvigsc7y9uj7vxemvti.png" alt="KhataLens: three khata pages read into one ledger, each row tagged with its page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The hisaab, and a reminder drafted by Gemma.&lt;/strong&gt; After all three pages, Ramesh owes ₹240, Pooja ₹150, Imran ₹110 and Arjun ₹40, while Kavita and Sunita are settled: &lt;strong&gt;₹540 to collect&lt;/strong&gt; (₹1,700 udhaar minus ₹1,160 jama). Pick a customer and a tone, and Gemma writes the message:&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%2Fw7tr8l8jy8x8b7dgsmdu.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%2Fw7tr8l8jy8x8b7dgsmdu.png" alt="Hisaab across three pages and a Gemma-drafted Hinglish reminder" width="800" height="500"&gt;&lt;/a&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/rahultapase" rel="noopener noreferrer"&gt;
        rahultapase
      &lt;/a&gt; / &lt;a href="https://github.com/rahultapase/KhataLens" rel="noopener noreferrer"&gt;
        KhataLens
      &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;KhataLens&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Photograph a page of a handwritten udhaar khata. Gemma 4 reads it on your laptop, turns it into
an editable ledger, totals what each customer owes, and drafts a polite Hinglish payment reminder
you can copy or open in WhatsApp. Nothing is sent automatically and nothing leaves the laptop.&lt;/p&gt;
&lt;p&gt;Built for &lt;strong&gt;Best Use of Gemma 4&lt;/strong&gt; at Hacktoberfest 2026 Mumbai.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/rahultapase/KhataLens/docs/screenshots/01-workspace.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%2Frahultapase%2FKhataLens%2FHEAD%2Fdocs%2Fscreenshots%2F01-workspace.png" alt="KhataLens: three khata pages read into one ledger, each row tagged with its page"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Small shops across India run on credit. A regular customer takes atta today and pays at the end
of the week, and the shopkeeper writes it in a paper udhaar notebook: date, name, item, amount
and "jama" when money comes back. Working out who owes how much means re-adding pages of
handwriting, and asking for money is awkward, so dues slip.&lt;/p&gt;
&lt;p&gt;Existing ledger apps expect the shopkeeper to retype everything. KhataLens starts from the
notebook he already keeps.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Photos in.&lt;/strong&gt; Take or drop one…&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/rahultapase/KhataLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Vite and TypeScript with no runtime dependencies, plain HTML and CSS, and Gemma 4 E2B through Ollama. There is no backend.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;One small open model, two jobs.&lt;/strong&gt; KhataLens uses &lt;code&gt;gemma4:e2b&lt;/code&gt; (about 4.6B parameters, Q4_K_M) through Ollama's &lt;code&gt;/api/chat&lt;/code&gt;. The browser reaches Ollama through Vite's dev proxy, so there's no server, no API key and no CORS setup. On my laptop's 4 GB RTX 2050, a page takes about 6 to 9 seconds once the model is loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reading the page: vision with a JSON schema.&lt;/strong&gt; The photo is downscaled in the browser to a 1280 px JPEG and sent with a JSON schema in Ollama's &lt;code&gt;format&lt;/code&gt; field, so Gemma has to answer as structured data. Thinking is off and the temperature is 0.1. The system prompt explains khata conventions: a name with an amount is udhaar unless the line says jama, paid, diya or received; read numbers literally and never calculate; one entry per written line, never invent rows; set unclear fields to null; count the lines you can't read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The bug that shaped the schema.&lt;/strong&gt; My first schema asked for customer, amount, kind, date and item directly. On my handwritten pages, Gemma read two short payment lines like "Pooja jama 100" as udhaar. That's the worst kind of error for a khata: it &lt;em&gt;adds&lt;/em&gt; ₹100 to Pooja's balance instead of taking it off.&lt;/p&gt;

&lt;p&gt;The fix was to make Gemma copy the whole line before splitting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Exact transcription first: the model reads the whole line before splitting it,&lt;/span&gt;
  &lt;span class="c1"&gt;// and the app can check it for payment words itself.&lt;/span&gt;
  &lt;span class="nl"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;udhaar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jama&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;Then code applies the khata rule to each line instead of trusting the model's label: a payment word (jama, paid, diya, received...) in the copied line means jama, and a real item of goods means udhaar.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Result on 3 handwritten pages (24 lines)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First schema&lt;/td&gt;
&lt;td&gt;2 short jama lines read as udhaar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copy-the-line-first schema + code rule&lt;/td&gt;
&lt;td&gt;every name, amount, date and udhaar/jama correct, in each of 3 runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3. Guardrails around the model.&lt;/strong&gt; Invalid JSON gets one retry; after that, Gemma's raw answer is shown instead of a blank error. The model is warmed up as soon as the app opens, and a status pill shows "Gemma loading" or "Gemma 4 ready". The editable table is the core of the design, not a fallback: handwriting is messy, so the photo and the rows are always side by side. If the same page is added twice, the app asks before counting it again. For that it stores only a SHA-256 fingerprint of each resized photo, never the photo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Reminders: Gemma writes the words, code checks the facts.&lt;/strong&gt; Gemma gets the customer, the amount, the shop name and a tone (polite, friendly, or firm but respectful), and writes at most 60 words of Roman-script Hinglish. The prompt rules out threats, shaming, legal words and any mention of other customers. Before a draft is shown, code checks that it contains the name, the exact amount and the shop name, and no Devanagari. If not, it retries once, then falls back to a fixed template. The UPI line is added by code, never by the model, so it's always exact. If a later page or an edit changes the balance, the draft is marked outdated, so an old amount isn't sent by mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Small security details.&lt;/strong&gt; Model output goes into the page as text, never as HTML. The CSV export writes any cell starting with &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;@&lt;/code&gt; as text, so a customer name can't turn into a spreadsheet formula. Ollama stays on localhost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the work was split.&lt;/strong&gt; I built it solo with an AI coding agent (Kiro), phase by phase from a written plan: the Gemma core first, tested on a handwritten photo before any UI existed, then the ledger, reminders, a design pass and multi-page reading. I made the calls on scope, the khata rules and the tests, and the agent did much of the typing.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;A khata is other people's financial data.&lt;/strong&gt; It records which neighbour bought what and who hasn't paid. That isn't the shopkeeper's data to upload to a server he doesn't control, and "we send your customers' dues to an AI company" is not a pitch a kirana owner will accept. With an open-weight model running in Ollama, the photo, the names and the amounts stay on his computer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero cost per page is what makes the price work.&lt;/strong&gt; A hosted vision API charges for every image, so the bill would grow with every page he photographs and every reminder he drafts. Local inference makes that cost zero. A flat monthly price doesn't get eaten by usage, and the shopkeeper never sees a surprise bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't depend on the shop's connection.&lt;/strong&gt; After &lt;code&gt;ollama pull gemma4:e2b&lt;/code&gt;, reading pages and writing reminders work offline. Only the final WhatsApp step needs the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could shape the whole pipeline, not just the prompt.&lt;/strong&gt; JSON-schema output, thinking off for speed, low temperature, and the copy-the-line-first schema that fixed the jama bug. Because the model is small, one model covers both jobs, reading handwriting and writing Hinglish, on a laptop with a 4 GB GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where closed would be better, to be honest.&lt;/strong&gt; A large hosted model would probably read messier handwriting better. KhataLens makes up for that by keeping the photo and the editable row side by side, so a misread costs a two-second fix instead of a wrong balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;It was tested on three pages (24 lines) in my own handwriting, with made-up names. Real khatas are messier: cramped lines, crossed-out entries, running totals.&lt;/li&gt;
&lt;li&gt;It handles Roman-script Hinglish with English numerals only. Devanagari and Marathi notebooks aren't supported yet.&lt;/li&gt;
&lt;li&gt;Customers are matched by name (trimmed, case-insensitive), so "Ramesh" and "Rameshbhai" become two customers.&lt;/li&gt;
&lt;li&gt;It needs a computer with Ollama, and many shopkeepers only have a phone.&lt;/li&gt;
&lt;li&gt;No shopkeeper has used it yet.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Put it in front of a real shopkeeper with a real khata, and ask whether he'd pay. After that: Devanagari pages, a phone version (E2B is the smallest Gemma 4 size, so it's worth testing on a phone), voice entry ("Ramesh, atta paanch kilo, do sau pachaas"), and sending through the WhatsApp Business API with the shopkeeper's approval for every message.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 E2B does both jobs locally: vision with structured output to read handwritten khata pages, and writing Hinglish payment reminders.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Log Off: One Real Place. One Small Activity. Local Gemma.</title>
      <dc:creator>Rahul Tapase</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:14:00 +0000</pubDate>
      <link>https://dev.to/rahultapase/log-off-one-real-place-one-small-activity-local-gemma-40m3</link>
      <guid>https://dev.to/rahultapase/log-off-one-real-place-one-small-activity-local-gemma-40m3</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;At 4:00 PM in Kharghar, I gave a local model a deceptively small job: stop offering me choices and choose one real place I could walk to and return from in 30 minutes. The map pipeline found CIDCO Park seven minutes south, the timing code kept the outing inside daylight, and the interface reduced everything to one ticket. That test became &lt;strong&gt;Log Off&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Log Off turns four quick choices into one reason to close the laptop. You choose your mood, how much time you have, your energy, and whether you are alone. It then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;finds real nearby outdoor places from OpenStreetMap;&lt;/li&gt;
&lt;li&gt;checks walking time, current weather, sunrise, and sunset;&lt;/li&gt;
&lt;li&gt;asks a local Gemma 4 model for one small activity at a place that actually fits; and&lt;/li&gt;
&lt;li&gt;shows one ticket with where to go, what to do, when to leave, and when to be back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model does not decide distances or invent the schedule. Code owns those facts. The goal is to make the screen the shortest part of the experience.&lt;/p&gt;

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

&lt;p&gt;Log Off runs locally on a laptop, so there is no hosted demo. Setup and run instructions are in the README. Here is the complete screen flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Pick what you need from the break.&lt;/strong&gt; Four compact choices give the planner enough context without turning the break into another questionnaire.&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%2F0a4410xgjhas0mpulnut.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%2F0a4410xgjhas0mpulnut.png" alt="Log Off preference form" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Take one ticket and leave the screen.&lt;/strong&gt; The result gives one real place, one activity, and the leave-by and back-by times calculated by code.&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%2F0ambc4oct3qkyyrg9081.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%2F0ambc4oct3qkyyrg9081.png" alt="Log Off outing ticket" width="800" height="500"&gt;&lt;/a&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/rahultapase" rel="noopener noreferrer"&gt;
        rahultapase
      &lt;/a&gt; / &lt;a href="https://github.com/rahultapase/log-off" rel="noopener noreferrer"&gt;
        log-off
      &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;
&lt;a rel="noopener noreferrer" href="https://github.com/rahultapase/log-off/frontend/public/favicon.svg"&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%2Frahultapase%2Flog-off%2FHEAD%2Ffrontend%2Fpublic%2Ffavicon.svg" height="40" alt="Log Off logo"&gt;&lt;/a&gt; Log Off&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Log Off turns a few preferences into one short reason to leave the screen. Pick a mood, the time
available, your energy, and whether you are alone. The app finds a real nearby place, works out a
safe walking window around daylight and weather, and asks a local Gemma 4 model for one small
activity to do there.&lt;/p&gt;
&lt;p&gt;The result is a single outing ticket with where to go, when to leave, when to be back, and a task
that needs no phone, purchase, or equipment.&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05" rel="nofollow"&gt;DEV Hacktoberfest Open-Source AI Challenge, Week 1: Touch
Grass&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Screenshots&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/rahultapase/log-off/screenshots/01-ask.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%2Frahultapase%2Flog-off%2FHEAD%2Fscreenshots%2F01-ask.png" width="49%" alt="Log Off preference form"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/rahultapase/log-off/screenshots/03-place.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%2Frahultapase%2Flog-off%2FHEAD%2Fscreenshots%2F03-place.png" width="49%" alt="Log Off outing ticket"&gt;&lt;/a&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;p&gt;Log Off keeps facts and generation separate:&lt;/p&gt;


&lt;ol&gt;

&lt;li&gt;

&lt;strong&gt;Find facts.&lt;/strong&gt; A typed area or browser location is rounded to three decimal places before the
app requests nearby places from OpenStreetMap and daylight and weather data from Open-Meteo.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Build a shortlist.&lt;/strong&gt; Python code calculates walking time and…&lt;/li&gt;

&lt;/ol&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/rahultapase/log-off" 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;FastAPI, TypeScript, Vite, local Gemma 4 through Ollama, and 104 backend tests.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/rahultapase/log-off" rel="noopener noreferrer"&gt;github.com/rahultapase/log-off&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Log Off has three deliberate layers: facts, generation, and validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Facts before AI.&lt;/strong&gt; A FastAPI backend uses OpenStreetMap for geocoding and nearby places, then Open-Meteo for weather, sunrise, and sunset. Python code rounds outbound coordinates, calculates walking time and compass direction, merges duplicate map entries, and filters out anything that cannot fit the selected time budget.&lt;/p&gt;

&lt;p&gt;That produces a numbered shortlist of real places. It also decides whether the outing can happen now or should move to just after sunrise. Gemma never calculates any of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A local Gemma task writer.&lt;/strong&gt; Gemma 4 E2B runs locally through Ollama. It receives mood, energy, company, weather, and the numbered shortlist, but no coordinates. Its job is narrow: select one candidate ID and return a short, equipment-free activity as structured JSON.&lt;/p&gt;

&lt;p&gt;I found that the model was much more useful after code supplied a task style for each mood and energy combination. The first prompt often returned variations of "relax in the park." The final prompt asks for a concrete, finishable observation, investigation, movement, or social task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A factual guardrail.&lt;/strong&gt; Every model response passes through a validator that checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;valid JSON and schema;&lt;/li&gt;
&lt;li&gt;a place ID from the supplied shortlist;&lt;/li&gt;
&lt;li&gt;an activity duration that fits after the round-trip walk;&lt;/li&gt;
&lt;li&gt;invented place or street names;&lt;/li&gt;
&lt;li&gt;forbidden distances and clock times; and&lt;/li&gt;
&lt;li&gt;extra safety constraints for darkness and rain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rejected response gets one repair attempt. If Ollama is unavailable or both attempts fail, a built-in deterministic activity keeps the ticket usable. The explanation visible to the user is also generated from code facts, not from Gemma's free-text reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. I measured it.&lt;/strong&gt; For the main evaluation, I ran 24 combinations of mood, time, company, and energy at 4:00 PM:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result with Gemma 4 E2B&lt;/th&gt;
&lt;th&gt;Measured value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valid on the first attempt&lt;/td&gt;
&lt;td&gt;23 of 24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed on the single retry&lt;/td&gt;
&lt;td&gt;1 of 24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic fallback used&lt;/td&gt;
&lt;td&gt;0 of 24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invented-place runs&lt;/td&gt;
&lt;td&gt;0 of 24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median model-call latency&lt;/td&gt;
&lt;td&gt;2.8 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum model-call latency&lt;/td&gt;
&lt;td&gt;3.2 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most useful failure was not a hallucination. For a social, with-others, low-energy request, Gemma suggested: "Start a simple card game like Uno." It passed the factual validator but needed equipment, despite the prompt. The validator can prove that an answer fits known facts; it cannot yet prove that the idea is good. That distinction is now an explicit limitation instead of a hidden one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. I swapped the model.&lt;/strong&gt; With the same 12 preference sets, &lt;code&gt;gemma3:1b&lt;/code&gt; passed &lt;strong&gt;0 of 12&lt;/strong&gt; answers on the first attempt. Eleven were repaired and one fell back safely. Gemma 4 was much more reliable, but the open setup let me measure that difference inside the real application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. I kept the final screen quiet.&lt;/strong&gt; The browser interface is TypeScript and Vite. It uses a daylight arc, accessible controls, visible focus states, reduced-motion support, and one final screen designed to be left behind.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The model does not need my location.&lt;/strong&gt; Gemma runs on my laptop through Ollama, so no location is sent to a model provider, no model API key is needed, and inference has no per-request fee. I can inspect the full prompt, constrain its role, keep evaluation results locally, and replace the model without redesigning the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could test model sizes on the real task.&lt;/strong&gt; The Gemma 3 and Gemma 4 comparison used the same planner, validator, preference sets, and hardware. I could choose based on measured reliability instead of a general benchmark or pricing page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open data keeps generation grounded.&lt;/strong&gt; &lt;a href="https://www.openstreetmap.org/copyright" rel="noopener noreferrer"&gt;OpenStreetMap&lt;/a&gt; provides inspectable place IDs and geometry, while &lt;a href="https://open-meteo.com/en/license" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; provides weather and daylight data under an open licence. Together they let the model write around facts instead of pretending to know the user's surroundings.&lt;/p&gt;

&lt;p&gt;Open does not automatically mean offline. Place and weather lookups still need the internet. It does mean I can draw a clear privacy boundary: rounded coordinates go to open-data services, while Gemma stays local and never sees them. It also means the failure handling is mine to test, explain, and improve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The 24-run evaluation is a focused application test, not a general model benchmark.&lt;/li&gt;
&lt;li&gt;The validator checks facts and safety rules, not taste. The Uno suggestion passed.&lt;/li&gt;
&lt;li&gt;Weather describes current conditions even when an after-dark outing moves to tomorrow morning.&lt;/li&gt;
&lt;li&gt;Place and weather lookups need internet access, even though model inference is local.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Take Log Off on a real outing, add an equipment-language validator rule, use forecast data for next-morning plans, and compare more small local models on the same evaluation set.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 E2B writes the activity locally, and Gemma 3 1B provides the measured comparison.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>My mom runs tuition from a paper register. I built her one that reads Hinglish, runs offline, and never does the maths itself</title>
      <dc:creator>Rahul Tapase</dc:creator>
      <pubDate>Mon, 05 Oct 2026 06:54:30 +0000</pubDate>
      <link>https://dev.to/rahultapase/my-mom-runs-tuition-from-a-paper-register-i-built-her-one-that-reads-hinglish-runs-offline-and-1a78</link>
      <guid>https://dev.to/rahultapase/my-mom-runs-tuition-from-a-paper-register-i-built-her-one-that-reads-hinglish-runs-offline-and-1a78</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;My mom teaches home tuition. About 25 kids come to her, from class 1 to class 10.&lt;/p&gt;

&lt;p&gt;Everything lives in a paper register. Attendance goes in one column, fees in another, and in the margins there are notes like "paid half, rest next week". Parents message her on WhatsApp: "Ma'am 1000 bhej diya, baaki next week". At the end of the month she goes back through the register and the WhatsApp chats and adds up, by hand, who has paid for which month and who still owes. Then comes the part she likes least: reminding parents about fees without sounding rude.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Tuition Register&lt;/strong&gt;. She types a line the way she'd write it in the register:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;riya nahi aayi, aman ne 1500 diye oct ke, baaki 500 next week&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and it becomes three entries: Riya absent today, ₹1,500 from Aman for October, and ₹500 promised by next Monday. From those it keeps a month-by-month dues list and drafts a polite reminder she can copy into WhatsApp.&lt;/p&gt;

&lt;p&gt;The one rule I built everything around: &lt;strong&gt;Gemma reads, code counts.&lt;/strong&gt; The model copies words out of her line. It never computes a balance, a date or an amount. Code checks everything the model says, and anything it can't confirm goes to a &lt;strong&gt;Confirm tray&lt;/strong&gt; for one tap instead of being saved quietly.&lt;/p&gt;

&lt;p&gt;To be upfront: Mom hasn't used it yet. Everything below was tested on synthetic lines I wrote in her style. Sitting with her at the next month-end is the real test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;60 synthetic Hinglish register lines, 79 facts, &lt;strong&gt;measured with Wi-Fi off&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Gemma 4 E2B on its own got 94% of facts right (precision and recall)&lt;/li&gt;
&lt;li&gt;Without the checker it would have &lt;strong&gt;silently saved 7 wrong entries out of 79&lt;/strong&gt;. With it: &lt;strong&gt;2&lt;/strong&gt;, and 7 facts went to the Confirm tray instead&lt;/li&gt;
&lt;li&gt;3.3 s per line (p50) on a laptop with a 4 GB RTX 2050&lt;/li&gt;
&lt;li&gt;₹0 to run, no API key, nothing leaves the house&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;It runs locally on a laptop, so there's no live link. Setup takes about three commands and is in the README. Here's the flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. She writes a line. Code shows what the model picked out of it.&lt;/strong&gt; Green means checked, amber means "please look". The Neha card is amber because there are two students called Neha:&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%2Fiv8kz4t5w8l8kvfm4q75.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%2Fiv8kz4t5w8l8kvfm4q75.png" alt="Write tab with green and amber cards" width="780" height="3616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Confirm tray.&lt;/strong&gt; It asks which Neha. Nothing is saved until she picks one:&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%2Ftd7x5f3vofpjhnlahwvf.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%2Ftd7x5f3vofpjhnlahwvf.png" alt="Confirm tray asking which Neha" width="780" height="1726"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Dues for the month.&lt;/strong&gt; Who still owes, who's paid, and open promises:&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%2Fl3o4h1z7ad3sqm19i8m7.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%2Fl3o4h1z7ad3sqm19i8m7.png" alt="Dues tab" width="780" height="3508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Tap a student for their ledger and a reminder.&lt;/strong&gt; The model wrote the wording. Code put in "₹800" and "October":&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%2Fk12yrjzgpwxuv5boebno.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%2Fk12yrjzgpwxuv5boebno.png" alt="Student ledger and reminder message" width="780" height="1688"&gt;&lt;/a&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/rahultapase" rel="noopener noreferrer"&gt;
        rahultapase
      &lt;/a&gt; / &lt;a href="https://github.com/rahultapase/hacktoberfest-weekend-challenge" rel="noopener noreferrer"&gt;
        hacktoberfest-weekend-challenge
      &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;Tuition Register&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A tuition fee and attendance register my mom can write to like a notebook. She types a line the way she'd write it in her paper register (&lt;code&gt;riya nahi aayi, aman ne 1500 diye oct ke, baaki 500 next week&lt;/code&gt;). A &lt;strong&gt;local Gemma model (Ollama)&lt;/strong&gt; copies out the names, amounts and months as exact spans. &lt;strong&gt;Code&lt;/strong&gt; turns them into numbers and dates, checks everything (rules V1 to V8), and does all the money maths. Anything it can't confirm goes to a &lt;strong&gt;Confirm tray&lt;/strong&gt; instead of being saved. Runs fully offline.&lt;/p&gt;
&lt;p&gt;Built Oct 5, 2026 for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01" rel="nofollow"&gt;DEV Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;All data in this repo is synthetic.&lt;/strong&gt; The 18-student roster and the 60 test lines were written by me in the style of a home-tuition register. No real children's names or payments. Mom hasn't used it yet.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Write&lt;/th&gt;
&lt;th&gt;Confirm tray&lt;/th&gt;
&lt;th&gt;Dues&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&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/rahultapase/hacktoberfest-weekend-challenge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Python (FastAPI + SQLite), one HTML page with vanilla JS, 137 tests, and the eval set and results committed in &lt;code&gt;eval/&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Stack.&lt;/strong&gt; Gemma 4 E2B running in Ollama on my laptop, FastAPI, SQLite, and a single HTML page with vanilla JS and no build step. Every piece is small and offline. The page is mobile-first so it works from her phone over home Wi-Fi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The model copies, it doesn't convert.&lt;/strong&gt; Ollama's &lt;code&gt;format&lt;/code&gt; option takes a JSON schema, so the output is always valid JSON. The schema only asks for spans exactly as written:&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="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;"payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"student_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aman"&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_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"month_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;p&gt;Then plain Python takes over. &lt;code&gt;1.5k&lt;/code&gt;, &lt;code&gt;1,500&lt;/code&gt;, &lt;code&gt;15 sau&lt;/code&gt; and &lt;code&gt;dedh hazaar&lt;/code&gt; all become 1500. &lt;code&gt;pichle mahine&lt;/code&gt; becomes 2026-09. &lt;code&gt;next week&lt;/code&gt; becomes a date. &lt;code&gt;kal&lt;/code&gt; means both "yesterday" and "tomorrow" in Hindi, so code reads it as the past for a payment and the future for a promise. Nicknames like "Riyu" and "Amu" are matched against the roster with rapidfuzz.&lt;/p&gt;

&lt;p&gt;Because the model only returns spans, every claim it makes can be checked against what she actually typed. The app even shows her line back with those words highlighted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The checker (V1 to V8).&lt;/strong&gt; Every extracted fact runs through these rules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;Every span actually appears in her line (catches made-up values)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V2 / V3&lt;/td&gt;
&lt;td&gt;The amount parses, and it isn't more than 6× that student's fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V4&lt;/td&gt;
&lt;td&gt;The name matches exactly one student, clearly ahead of the next best&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V5 / V6&lt;/td&gt;
&lt;td&gt;The month or date resolves, and required fields are present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;V7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Coverage:&lt;/strong&gt; every student named in the line appears in some entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V8&lt;/td&gt;
&lt;td&gt;Not already saved today&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;V7 is my favourite. Most checkers only look at what the model produced, but this one looks for what it &lt;em&gt;left out&lt;/em&gt;. If she writes "mohit, arjun aur shreya nahi aaye" and the model returns only Mohit, she gets a card saying "You mentioned Arjun. Nothing saved for them."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. All the money maths is in code.&lt;/strong&gt; A payment goes to the month she named. Without a month, it goes to the oldest unpaid month, and anything extra carries forward as advance. One payment can cover two months ("sep oct dono ke"). Promises are tracked, but they never change a balance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Reminders: the model writes the tone, code writes the numbers.&lt;/strong&gt; Gemma writes a template like &lt;code&gt;Namaste ji, {name} ki {month} ki fees ₹{amount} baaki hai…&lt;/code&gt; in Hinglish, English or Hindi, in a gentle, normal or firm-but-polite tone. Code rejects any template with a digit, a missing placeholder or the wrong language, and falls back to a built-in one. On the first try, E2B wrote "Hi [Parent's Name]…" in English when I asked for Hinglish, so the validator now checks for leftover brackets and for the language too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. I measured it.&lt;/strong&gt; I wrote 60 synthetic lines in her style: single and multi-student attendance, payments in words ("aath sau"), partial payments with promises, WhatsApp messages from parents, typos ("kavya n 1500 diy"), English lines, chit-chat with nothing to save, and a few traps. I labelled them by hand from the rules, not by running the model. The few-shot examples in the prompt use different names, and a test enforces that. I ran it with Wi-Fi off, and the script records that the internet was unreachable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Fact precision&lt;/th&gt;
&lt;th&gt;Fact recall&lt;/th&gt;
&lt;th&gt;Silent errors, no checker&lt;/th&gt;
&lt;th&gt;Silent errors, with checker&lt;/th&gt;
&lt;th&gt;Sent to Confirm tray&lt;/th&gt;
&lt;th&gt;p50 latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (default)&lt;/td&gt;
&lt;td&gt;94%&lt;/td&gt;
&lt;td&gt;94%&lt;/td&gt;
&lt;td&gt;7 of 79&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7 facts (9%)&lt;/td&gt;
&lt;td&gt;3.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;td&gt;34 of 73&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;42 facts (58%) + 7 "missed?"&lt;/td&gt;
&lt;td&gt;3.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some things the checker stopped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rahul ne 1000 diye&lt;/code&gt;: there's no Rahul in the roster. A plain closest-name lookup would have credited ₹1,000 to &lt;strong&gt;Aman&lt;/strong&gt;. V4 asked "New student?" instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aman ne 15000 diye&lt;/code&gt;: probably a typo for 1500. V3 flagged it as unusual instead of clearing ten months of fees.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;neha absent&lt;/code&gt;: two students are called Neha. It asked which one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the 2 it didn't catch, because honesty matters more here than a clean table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Ma'am Ishaan kal nahi aayega&lt;/code&gt; ("won't come tomorrow"). Code reads &lt;code&gt;kal&lt;/code&gt; as yesterday for attendance, so the absence landed on the wrong day.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rohan ne 1200 jama kiye is mahine ke&lt;/code&gt;. The model dropped "is mahine ke" (this month), so the payment went to the oldest unpaid month instead of October. Rohan's total is still right, but the month is wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Two Gemmas side by side.&lt;/strong&gt; Gemma 3 1B is a bit faster, but it often copied the whole line into the name field, mixed up payments and attendance, and dropped 38 facts. The checker still kept its silent errors down to 4, but at the cost of sending more than half of everything to the tray, and that's not something Mom would put up with. E2B is the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. How it was built.&lt;/strong&gt; I worked with an AI coding agent (Kiro) through a spec I wrote: the rules, the schema, the ledger rules and the eval design. I made the calls on scope, rules and tests, and the agent did a lot of the typing.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;These are children's names and their parents' payments.&lt;/strong&gt; They aren't mine or my mom's to upload to someone else's server. With an open-weight model running in Ollama, nothing leaves the laptop. I didn't just assume that: the eval ran with Wi-Fi off, and the results file records it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs ₹0 to run, for good.&lt;/strong&gt; No API key, no subscription, no per-call bill. That matters for a home tuition business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could choose the model to fit the machine.&lt;/strong&gt; The laptop has a 4 GB GPU. Because the weights are open, I could try a 1B model and the newer E2B on the same 60 lines, with the same checker on the same hardware, and pick based on numbers rather than a pricing page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could shape the output itself.&lt;/strong&gt; JSON-schema output, temperature 0, thinking turned off for speed, and a prompt tuned for Hinglish register notes. The checker trusts the model only as far as it can verify, which is easier when you control the whole pipeline.&lt;/p&gt;

&lt;p&gt;With a closed API, every one of these would have been harder or impossible: uploading kids' data, paying per line, needing internet at the tuition table, and no way to compare model sizes on my own machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The test set is small and &lt;strong&gt;synthetic&lt;/strong&gt;: 60 lines I wrote in her style. It's a sanity check, not a benchmark, and her real writing will be messier.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kal&lt;/code&gt; (yesterday or tomorrow) is still a guess for attendance. She can fix it in the tray or the day's list.&lt;/li&gt;
&lt;li&gt;Typing Hindi in Devanagari script hasn't been tested yet.&lt;/li&gt;
&lt;li&gt;LAN mode for her phone has only a simple shared PIN. It's for a home network only.&lt;/li&gt;
&lt;li&gt;No voice input yet.&lt;/li&gt;
&lt;li&gt;Mom hasn't used it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Hand it to Mom and test it on her real month-end. Then: voice notes through a local Whisper, Devanagari input, and a one-page monthly summary.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 E2B is the default model and the core of the app. Gemma 3 1B is the comparison.&lt;/li&gt;
&lt;/ul&gt;

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