<?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: Sameer</title>
    <description>The latest articles on DEV Community by Sameer (@theplator).</description>
    <link>https://dev.to/theplator</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%2F1765090%2Fc55a3692-5e22-49e1-b6f5-eb8c9574aca4.jpeg</url>
      <title>DEV Community: Sameer</title>
      <link>https://dev.to/theplator</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/theplator"/>
    <language>en</language>
    <item>
      <title>प्रश्न पत्र</title>
      <dc:creator>Sameer</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:19:02 +0000</pubDate>
      <link>https://dev.to/theplator/prshn-ptr-2p0i</link>
      <guid>https://dev.to/theplator/prshn-ptr-2p0i</guid>
      <description>&lt;p&gt;It's 11:47 PM in Patna. Classes ended hours ago, and a coaching teacher is typing tomorrow's practice test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q1. Which Article abolishes untouchability?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then they type it again, because half the batch studies in Hindi:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;प्र.1 कौन-सा अनुच्छेद अस्पृश्यता का अंत करता है?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Twenty-five questions, twice, every week. Paid question banks don't match their own notes, and cloud AI tools charge per use, need good internet, and send the institute's notes to someone else's server.&lt;/p&gt;

&lt;p&gt;So for "Side Hustle", I built the tool a small coaching centre would actually pay for.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Prashn Patra&lt;/strong&gt; (प्रश्न पत्र, "question paper") turns a chapter into a ready-to-print practice test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In:&lt;/strong&gt; a chapter PDF, photos of book pages, or the teacher's own notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out:&lt;/strong&gt; a &lt;strong&gt;student paper&lt;/strong&gt; and an &lt;strong&gt;answer key&lt;/strong&gt;, in &lt;strong&gt;Hindi, English or bilingual&lt;/strong&gt; (both on one sheet, same answer letters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Question types:&lt;/strong&gt; MCQ, true/false, fill in the blank, short answer, at easy, medium or hard difficulty, 5 to 50 questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I care about most: &lt;strong&gt;every answer has to cite the chapter.&lt;/strong&gt; Each question carries the exact sentence it came from, the code checks that sentence really is in the chapter, and the answer key prints it beside the answer. A wrong answer key costs a teacher their credibility in front of students, so accuracy beats quantity.&lt;/p&gt;

&lt;p&gt;It runs &lt;strong&gt;Gemma 3 (4B) locally through Ollama&lt;/strong&gt;, on the institute's own laptop. No API bill, no internet after setup, and the notes never leave the room.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it's for:&lt;/strong&gt; coaching institute owners with 1–5 teachers and 50–500 students, and home tutors teaching several classes. In Patna that mostly means school boards (Bihar Board), BPSC, SSC and Railway exam prep.&lt;/p&gt;

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

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

&lt;p&gt;The flow is four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the chapter (PDF, photos or pasted notes).&lt;/li&gt;
&lt;li&gt;Pick the number of questions, types, difficulty and language, then press &lt;strong&gt;Generate test · टेस्ट बनाएं&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Review every question, with its ✅ answer and 📖 source line. Edit, delete or regenerate any of them.&lt;/li&gt;
&lt;li&gt;Tick &lt;em&gt;"I have reviewed all questions"&lt;/em&gt; and download two PDFs: the student paper and the answer key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Export stays locked until step 4. &lt;strong&gt;The app never prints a question the teacher hasn't seen.&lt;/strong&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/ThePlator" rel="noopener noreferrer"&gt;
        ThePlator
      &lt;/a&gt; / &lt;a href="https://github.com/ThePlator/Coaching_Test_Generator-" rel="noopener noreferrer"&gt;
        Coaching_Test_Generator-
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Turn a chapter into a printable practice test in Hindi, English or both — with an answer key that cites the source line. Runs offline with Gemma via Ollama.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Coaching Test Generator · प्रश्न पत्र&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Turn a chapter into a ready-to-print practice test in Hindi, English or both, with an answer key that shows where each answer comes from. It runs fully offline on your own laptop.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built for small coaching institutes and home tutors preparing students for &lt;strong&gt;school boards, BPSC, SSC and Railway&lt;/strong&gt; exams. Upload a chapter PDF, photos of book pages, or your own notes. Pick how many questions you want. A few minutes later you have a student paper and an answer key to print.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🏁 Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 2 — "Side Hustle". Uses &lt;strong&gt;Gemma&lt;/strong&gt; running locally through Ollama.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Writing 25 good questions for one chapter takes hours. Translating them for Hindi-medium students doubles the work.&lt;/li&gt;
&lt;li&gt;Paid question banks are generic and don't match &lt;strong&gt;your&lt;/strong&gt; notes.&lt;/li&gt;
&lt;li&gt;Cloud AI tools charge per use, need good internet, and send your notes…&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/ThePlator/Coaching_Test_Generator-" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Setup is one script (&lt;code&gt;scripts/setup.sh&lt;/code&gt; on Linux/macOS, &lt;code&gt;scripts/setup.ps1&lt;/code&gt; on Windows), or by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 3 4B&lt;/strong&gt; via Ollama&lt;/td&gt;
&lt;td&gt;Open weights, runs on a laptop CPU, reads photos too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;Python + &lt;strong&gt;Streamlit&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Upload, review and edit on one page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pypdf&lt;/code&gt; for text PDFs, &lt;strong&gt;Gemma vision&lt;/strong&gt; for photos and scanned pages&lt;/td&gt;
&lt;td&gt;Many coaching notes are phone photos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output checks&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Pydantic&lt;/strong&gt; + &lt;strong&gt;rapidfuzz&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Catch broken JSON and made-up answers in code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;Jinja2 + &lt;strong&gt;WeasyPrint&lt;/strong&gt;, embedded &lt;strong&gt;Noto Sans Devanagari&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Hindi has to print correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are already tools that turn notes into quizzes, like &lt;a href="https://dev.to/wonderlab/pen-source-project-of-the-day-part-23-pagelm-open-source-ai-education-platform-turning-2ge9"&gt;PageLM&lt;/a&gt;, and cloud question-paper generators built on &lt;a href="https://dev.to/saravana_dev/building-an-ai-question-paper-generator-conquering-google-cloud-document-ai-firestore-vector-4dpa"&gt;Gemini and Document AI&lt;/a&gt;. What I wanted was narrower: one printed paper, in exam Hindi and English, where a teacher can verify every answer in seconds, on hardware they already own.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. One small chunk at a time
&lt;/h3&gt;

&lt;p&gt;The chapter is split into topic-sized chunks of about 1,500 characters, at headings and paragraph breaks. Gemma only ever sees &lt;strong&gt;one chunk&lt;/strong&gt; and is told to use only facts from it. A small model hallucinates far less with a narrow context, and it gives every question exactly one piece of text to be checked against.&lt;/p&gt;

&lt;p&gt;Gemma answers in JSON. Ollama's structured outputs take a JSON schema, so the model can't skip the Hindi block in bilingual mode or forget the &lt;code&gt;source_quote&lt;/code&gt;. Hindi and English come from &lt;strong&gt;one call&lt;/strong&gt;, so they share one &lt;code&gt;answer_index&lt;/code&gt; by construction. Both versions always have the same correct letter, and the Hindi is written as exam Hindi, not translated word by word afterwards.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The check that matters: is the quote really in the chapter?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ctg/textutil.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quote_score&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;0-100 score for how well `quote` appears in `chunk` (100 = exact substring).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&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="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fuzz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial_ratio&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="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;norm()&lt;/code&gt; does the Hindi-specific work. It applies Unicode NFC, strips zero-width joiners, maps &lt;code&gt;।&lt;/code&gt; to &lt;code&gt;.&lt;/code&gt;, and drops punctuation while keeping matras and the virama, so a quote with slightly different punctuation still matches. Below 90, the question fails as &lt;code&gt;quote_not_found&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same validator also checks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exactly 4 distinct options, with the answer among them&lt;/li&gt;
&lt;li&gt;no "all of the above" (or &lt;em&gt;उपरोक्त सभी&lt;/em&gt; / &lt;em&gt;इनमें से कोई नहीं&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Hindi that's actually in Devanagari&lt;/li&gt;
&lt;li&gt;matching option counts in Hindi and English&lt;/li&gt;
&lt;li&gt;a real &lt;code&gt;____&lt;/code&gt; in fill-in-the-blank questions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Retry once, then drop, and tell the teacher
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ctg/pipeline.py (trimmed)
&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quota&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overgen_factor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# ask for ~30% extra
&lt;/span&gt;&lt;span class="n"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_for_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;expand_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qtypes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quota&lt;/span&gt; &lt;span class="o"&gt;-&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;good&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                           &lt;span class="c1"&gt;# retry ONCE for this chunk
&lt;/span&gt;    &lt;span class="n"&gt;cands2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lost2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_for_chunk&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;feedback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;FEEDBACK_LIMIT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="n"&gt;good2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cands2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;good&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;good2&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry prompt tells Gemma exactly what failed (&lt;em&gt;"source not found in chapter"&lt;/em&gt;, &lt;em&gt;"repeated options"&lt;/em&gt;). Anything still failing is dropped, and the review screen says so honestly, for example: &lt;em&gt;"Kept 9 of 10 — dropped: 1 source not found in chapter"&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A dedupe bug I'm glad I caught early
&lt;/h3&gt;

&lt;p&gt;My first duplicate filter used rapidfuzz's &lt;code&gt;token_set_ratio&lt;/code&gt;. It scored these two stems at 88, over my threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"Which Articles deal with the Right to **Equality&lt;/em&gt;&lt;em&gt;?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Which Articles deal with the Right to **Freedom&lt;/em&gt;&lt;em&gt;?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set-based matching mostly ignores the one word that makes them different questions, and on an exam paper that word is the whole point. &lt;code&gt;token_sort_ratio&lt;/code&gt; at 90 still catches real repeats and reordered wording, but keeps these apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Hindi that actually prints
&lt;/h3&gt;

&lt;p&gt;Devanagari isn't just characters. Conjuncts like &lt;strong&gt;क्ष, त्र, श्र&lt;/strong&gt; and matras need OpenType shaping, and drawing text without it can print broken Hindi. That's a disaster on a paper students will read. Rendering HTML through WeasyPrint gets shaping from HarfBuzz (via Pango), with Noto Sans Devanagari embedded in the PDF. On Windows, where WeasyPrint's GTK libraries are hard to install, the same HTML prints through Chromium instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Would Pay
&lt;/h2&gt;

&lt;p&gt;These are the prices I'm testing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Try it&lt;/td&gt;
&lt;td&gt;3 tests / month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tutor&lt;/td&gt;
&lt;td&gt;Home tutors&lt;/td&gt;
&lt;td&gt;₹199 / month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Institute&lt;/td&gt;
&lt;td&gt;Coaching centres&lt;/td&gt;
&lt;td&gt;₹999 / month, unlimited, set up on their laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-time setup&lt;/td&gt;
&lt;td&gt;Institutes without tech help&lt;/td&gt;
&lt;td&gt;₹1,500 install + training&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The software itself is MIT-licensed. People pay for setup, support and not having to do it themselves, which is how a lot of small-town software actually gets bought. These prices only work because &lt;strong&gt;running cost is close to zero&lt;/strong&gt;: the model runs on a laptop the institute already owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;[FILL — after the session, from docs/first-user.md. Suggested shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who: a coaching teacher or tutor in Patna (name only with permission), the subject and exam they teach.&lt;/li&gt;
&lt;li&gt;They uploaded their own chapter and made a 10-question bilingual test in [time]. Normally that takes them [their answer].&lt;/li&gt;
&lt;li&gt;Of 10 questions: kept [k], edited [e], marked [w] wrong, and why.&lt;/li&gt;
&lt;li&gt;Their verdict on the Hindi.&lt;/li&gt;
&lt;li&gt;"Would you pay ₹199 a month? ₹999 for the institute?" Their exact words.&lt;/li&gt;
&lt;li&gt;What they asked for next.&lt;/li&gt;
&lt;li&gt;Photo of the printed paper (no student names).]&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For this business, open weights aren't a nice extra. They're what makes the price possible.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Near-zero running cost.&lt;/strong&gt; A cloud API bills every question, every week, for every teacher. On the institute's own laptop, the hundredth test costs the same as the first: nothing. That's the difference between ₹199 a month being a business and being a loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The notes stay theirs.&lt;/strong&gt; A teacher's notes are their competitive edge in a crowded coaching market. Here they never leave the laptop, and you can check that: turn off the Wi-Fi and it still works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built for bad internet.&lt;/strong&gt; Power cuts and patchy mobile data are normal. Once set up, this needs neither.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunable for exam styles.&lt;/strong&gt; Open weights mean the model can later be fine-tuned on BPSC/SSC-style questions written by teachers themselves, a niche a general-purpose API handles poorly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; The model is one setting (&lt;code&gt;CTG_MODEL&lt;/code&gt;). If a better open model comes out, or a smaller one runs faster on an old laptop, the institute switches without asking anyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed depends on the laptop.&lt;/strong&gt; On a CPU-only machine, writing 10 bilingual questions takes [FILL — measured time]. Start with 10–15 questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 4B model still makes mistakes.&lt;/strong&gt; That's why review is required and the key shows the source line. The checks catch made-up quotes, but not every awkward distractor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text subjects only, for now.&lt;/strong&gt; GK, history, polity, science theory, English and Hindi work. Maths with diagrams or long derivations doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photos are slower and less reliable&lt;/strong&gt; than text PDFs or pasted notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested with a stand-in model.&lt;/strong&gt; The automated tests use a fake model so they run without Ollama. The real-model numbers above come from the first-user session.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 3 4B writes every question in Hindi and English and reads photos of book pages and scanned PDFs, fully offline, on one laptop.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;हर अध्याय के साथ, देर रात की मेहनत थोड़ी कम हो।&lt;/em&gt; (With every chapter, a little less late-night work.) 📝&lt;/p&gt;

&lt;p&gt;— Sameer&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>Chhath Ghat Guide: an offline AI helper for the day the network dies at the ghat</title>
      <dc:creator>Sameer</dc:creator>
      <pubDate>Sat, 10 Oct 2026 11:35:25 +0000</pubDate>
      <link>https://dev.to/theplator/chhath-ghat-guide-an-offline-ai-helper-for-the-day-the-network-dies-at-the-ghat-54dd</link>
      <guid>https://dev.to/theplator/chhath-ghat-guide-an-offline-ai-helper-for-the-day-the-network-dies-at-the-ghat-54dd</guid>
      <description>&lt;h2&gt;
  
  
  The ghat, the crowd and the dead network
&lt;/h2&gt;

&lt;p&gt;Every year during &lt;strong&gt;Chhath Puja&lt;/strong&gt;, lakhs of families walk down to the ghats of Bihar to offer &lt;em&gt;arghya&lt;/em&gt; to the setting and the rising sun. Vratis stand waist-deep in the river, children hold their parents' hands, elders watch the water, and the songs carry across the steps.&lt;/p&gt;

&lt;p&gt;And every year the same three things happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Families get &lt;strong&gt;separated&lt;/strong&gt; in the crowd.&lt;/li&gt;
&lt;li&gt;Notices about &lt;strong&gt;deep water, barricades and timings&lt;/strong&gt; are hard to read in the press of people — and many elders are more at home in Bhojpuri or Maithili than in printed Hindi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mobile network jams.&lt;/strong&gt; Calls don't connect, messages don't send, and every app that needs the internet stops working — exactly when it's needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a small helper for that moment. &lt;strong&gt;Chhath Ghat Guide&lt;/strong&gt; runs on &lt;strong&gt;one laptop at the ghat&lt;/strong&gt;. The laptop's Wi-Fi hotspot has &lt;strong&gt;no internet at all&lt;/strong&gt;; family phones join it and open the app in a browser. The AI model, the notes and every photo stay at the ghat.&lt;/p&gt;

&lt;p&gt;The screen is meant to be a few seconds of help, not the experience. The experience is the river, the sun and the family.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Three tabs, one job each. (Screens are from the real app on a phone-sized browser; see the note under each image about what's test data.)&lt;/p&gt;

&lt;h3&gt;
  
  
  📜 सूचना पढ़ें — Notice Reader
&lt;/h3&gt;

&lt;p&gt;Take a photo of a notice or banner. A local &lt;strong&gt;Gemma 3 (4B)&lt;/strong&gt; vision model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;copies the text exactly&lt;/strong&gt; as written, and&lt;/li&gt;
&lt;li&gt;explains it in &lt;strong&gt;simple Hindi, Bhojpuri or Maithili&lt;/strong&gt; — using &lt;em&gt;only&lt;/em&gt; the text it just read.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The original words are always on screen. &lt;strong&gt;Safety notices&lt;/strong&gt; (deep water, barricades, emergency) show the original text &lt;strong&gt;first, inside a red box&lt;/strong&gt; — never only a summary. A blurry photo gets &lt;strong&gt;"साफ़ नहीं पढ़ पाए"&lt;/strong&gt; ("couldn't read it clearly") and tips for a better photo, instead of a guess. And if the explanation contains a number that isn't in the notice — an invented ghat number or time — the app warns you.&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%2Fdxz3cf91rzl2mh202ysu.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%2Fdxz3cf91rzl2mh202ysu.png" alt="Notice Reader: a safety notice with the original text in red, an ordinary notice with a simple explanation, and a blurry photo the app refuses to guess at" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Real app screens; the notices are printed samples, and the read text and explanations shown are test data copied from the notices (screens with Gemma's own output will replace these after the field test).&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🪔 पूजा सहायक — Ritual Helper
&lt;/h3&gt;

&lt;p&gt;Ask &lt;em&gt;"सूप में क्या रखते हैं?"&lt;/em&gt; (what goes in the soop?) or &lt;em&gt;"खरना में क्या बनता है?"&lt;/em&gt;. Answers come &lt;strong&gt;only from ten short notes written by the family and checked by an elder&lt;/strong&gt; — not from the model's memory. Each answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shows which note it came from and &lt;strong&gt;who checked it&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;ends with a reminder that &lt;strong&gt;customs differ between families and regions&lt;/strong&gt;, and&lt;/li&gt;
&lt;li&gt;falls back to &lt;strong&gt;"यह मेरे नोट्स में नहीं है — किसी बड़े से पूछें"&lt;/strong&gt; ("not in my notes — ask an elder") when the notes don't cover it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about &lt;strong&gt;clock times, this year's dates or parking never reach the model at all&lt;/strong&gt; — they're sent to official notices and the panchang. An AI should not be the one telling a vrati what time to stand in the river.&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%2Fc9k52c7qlgp6ovbm71xk.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%2Fc9k52c7qlgp6ovbm71xk.png" alt="Ritual Helper: an answer with its source note, the " width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Real app screens; the answer text is test data copied from the note it cites. The notes are marked "डेमो — समीक्षा बाकी" (demo — review pending) because the elder review comes before the real demo.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  📍 मिलने की जगह — Meeting Spot
&lt;/h3&gt;

&lt;p&gt;"&lt;strong&gt;Save our spot&lt;/strong&gt;" stores the GPS position, a photo of a landmark and a note (&lt;em&gt;"नीला टेंट, घाट 5 की सीढ़ियों के पास"&lt;/em&gt; — blue tent by the ghat 5 steps) &lt;strong&gt;on the phone only&lt;/strong&gt;. "&lt;strong&gt;Take me back&lt;/strong&gt;" shows the distance and an &lt;strong&gt;arrow that turns with the phone's compass&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GPS comes from satellites, not the mobile network, so this keeps working when calls don't — and because it runs entirely in the phone's browser, it keeps working even if the phone wanders out of the laptop's Wi-Fi. When you're within GPS error of the spot, the arrow would just be noise, so it switches to &lt;em&gt;"आप बहुत पास हैं — फोटो वाली निशानी देखें"&lt;/em&gt; (you're very close — look for the landmark in the photo). The spot deletes itself at midnight.&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%2F5fo7kh598lu6w91myaw7.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%2F5fo7kh598lu6w91myaw7.png" alt="Meeting Spot: saved spot with landmark photo, the compass arrow showing about 130 m north-east, and the " width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Real app screens with emulated GPS and compass (the landmark is a drawing).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;emergency card&lt;/strong&gt; sits on every screen and sends people to the nearest police or volunteer booth. The app ships with &lt;strong&gt;no phone numbers&lt;/strong&gt; — organisers add verified ones on the day; a made-up number at a crowded river is worse than none.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works offline
&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%2F71cu7nxgrg0e8afdx4p5.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%2F71cu7nxgrg0e8afdx4p5.png" alt="Architecture: phones join the laptop's hotspot; the Streamlit app talks to Ollama on 127.0.0.1; the Meeting Spot runs on the phone with GPS from satellites" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 3 4B&lt;/strong&gt; via Ollama, for both reading photos and writing answers&lt;/td&gt;
&lt;td&gt;Open weights, runs on a laptop CPU, no internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;Python + &lt;strong&gt;Streamlit&lt;/strong&gt;, three tabs&lt;/td&gt;
&lt;td&gt;Fast to build; camera and file inputs built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ritual answers&lt;/td&gt;
&lt;td&gt;10 Markdown notes, keyword + &lt;code&gt;nomic-embed-text&lt;/code&gt; search&lt;/td&gt;
&lt;td&gt;Answers traceable to checked text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meeting Spot&lt;/td&gt;
&lt;td&gt;Plain HTML + JS, Geolocation + DeviceOrientation APIs&lt;/td&gt;
&lt;td&gt;Runs on the phone; location never leaves it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phones&lt;/td&gt;
&lt;td&gt;Self-signed &lt;strong&gt;HTTPS&lt;/strong&gt; on the hotspot&lt;/td&gt;
&lt;td&gt;Phone browsers only allow GPS, camera and compass on secure pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The code that keeps the AI honest
&lt;/h2&gt;

&lt;p&gt;The interesting part of this project wasn't getting Gemma to talk — it was making sure it &lt;strong&gt;doesn't say more than it knows&lt;/strong&gt;, in a place where a wrong answer can matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Read first, then explain only what was read
&lt;/h3&gt;

&lt;p&gt;The Notice Reader makes two calls on purpose. The first copies the text (temperature 0, unreadable words marked &lt;code&gt;[?]&lt;/code&gt;); the second explains &lt;strong&gt;only that text&lt;/strong&gt;, so every claim on screen can be checked against the original right below it.&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;# ghat_guide/notice_reader.py (trimmed)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Transcription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vision_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SYS_TRANSCRIBE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# "copy exactly; write [?] for unreadable words"
&lt;/span&gt;        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_TRANSCRIBE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TRANSCRIBE_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# {text_read, script, clarity}
&lt;/span&gt;        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;clarity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clarity_verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clarity&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;span class="c1"&gt;# model's opinion + our own rules
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Transcription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_read&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;span class="n"&gt;clarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;is_safety_notice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_read&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;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Transcription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Explanation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clarity&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unclear&lt;/span&gt;&lt;span class="sh"&gt;"&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;None&lt;/span&gt;                          &lt;span class="c1"&gt;# nothing reliable to explain: show "साफ़ नहीं पढ़ पाए"
&lt;/span&gt;    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sys_explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                             &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_read&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Explanation&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;number_warning&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;unsupported_numbers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;explanation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_read&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Splitting the steps had a nice side effect: switching language re-runs only the cheap text step, not the slow image step.&lt;/p&gt;

&lt;p&gt;The safety check is plain keyword matching, chosen carefully — &lt;em&gt;"डूब"&lt;/em&gt; (drown) alone would also match &lt;em&gt;"डूबते सूर्य"&lt;/em&gt;, the &lt;strong&gt;setting sun&lt;/strong&gt; that Sandhya Arghya is offered to, so the list uses &lt;em&gt;"डूबने"&lt;/em&gt; instead:&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;# ghat_guide/safety.py (excerpt)
#   "डूब" alone would match "डूबते सूर्य" (the setting sun of Sandhya Arghya),
#   "मना" alone would match "मनाया" (celebrated).
&lt;/span&gt;&lt;span class="n"&gt;SAFETY_TERMS_DEVANAGARI&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;गहरा&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;गहराई&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;डूबने&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;खतरा&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;बैरिकेड&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;निषेध&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;मना है&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;h3&gt;
  
  
  2. Ritual answers have to earn their way out
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ghat_guide/ritual/answerer.py (trimmed)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RitualAnswer&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;asks_for_official_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;       &lt;span class="c1"&gt;# "बजे", "तारीख", "इस साल", "पार्किंग", ...
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RitualAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;official_info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# never asks the model
&lt;/span&gt;
    &lt;span class="n"&gt;relevant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_relevant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RitualAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_json&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_ritual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                             &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RITUAL_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# {found, answer, used_chunk_ids}
&lt;/span&gt;    &lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;cited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used_chunk_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cited&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RitualAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# no sources, or sources it wasn't given
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RitualAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search is a mix of keywords and embeddings, because &lt;code&gt;nomic-embed-text&lt;/code&gt; is mostly trained on English and Devanagari questions alone can rank poorly. Hand-written keywords plus a small synonym map (&lt;em&gt;अरघ → अर्घ्य&lt;/em&gt;, &lt;em&gt;सूपा → सूप&lt;/em&gt;) and a light stemmer (&lt;em&gt;खरने ↔ खरना&lt;/em&gt;) catch the words families actually use. And a note without an elder's &lt;code&gt;checked_by&lt;/code&gt; is never loaded at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The arrow is just two lines of trigonometry
&lt;/h3&gt;

&lt;p&gt;The Meeting Spot runs in the phone's browser, so it's plain JavaScript:&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="c1"&gt;// static/meeting_spot.html (excerpt, simplified)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bearing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lon1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lon2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;          &lt;span class="c1"&gt;// degrees clockwise from north&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;p1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lat1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lon2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lon1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rad&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;rad&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// heading comes from the compass (iOS: webkitCompassHeading, Android: 360 - alpha),&lt;/span&gt;
&lt;span class="c1"&gt;// smoothed with a circular mean so 350° and 10° average to 0°, not 180°.&lt;/span&gt;
&lt;span class="nx"&gt;arrow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rotate(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;mod360&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bearing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;here&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deg)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same maths lives in Python (&lt;code&gt;ghat_guide/geo.py&lt;/code&gt;), and a test runs both side by side in a real browser so they can't drift apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A pre-trip check, because you can't download anything at the ghat
&lt;/h3&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%2Flh8yqzcbj9afeym3gpqp.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%2Flh8yqzcbj9afeym3gpqp.png" alt="Output of scripts/check_offline.py: models downloaded, phone pages offline-only, warnings for unchecked notes and missing emergency contacts" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A real run of &lt;code&gt;scripts/check_offline.py&lt;/code&gt; — the warnings are true: the notes are still waiting for the elder review.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It checks that the models are downloaded, the ritual index is built, the phone pages load &lt;strong&gt;nothing&lt;/strong&gt; from the internet, the HTTPS certificate matches the hotspot's IP (a classic "it worked at home" failure), and that the model server isn't reachable from the phones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Touching grass
&lt;/h2&gt;

&lt;p&gt;[FILL — after the field test. Suggested shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where: the ghat or riverbank, the date, who came along.&lt;/li&gt;
&lt;li&gt;The moment you switched mobile data off and the app kept working.&lt;/li&gt;
&lt;li&gt;Numbers from docs/FIELD_TEST.md: seconds per notice, whether the arrow pointed the right way, GPS accuracy near the water.&lt;/li&gt;
&lt;li&gt;What the elder said about the Ritual Helper (with permission), and what they corrected in the notes.&lt;/li&gt;
&lt;li&gt;What didn't work — honestly.]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real test is still ahead: Chhath Puja is in late October / early November, and I'll take it to the ghat then and report back here.&lt;/p&gt;

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

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

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It works with no network.&lt;/strong&gt; A cloud AI is useless when the network is jammed; an open-weight model on a laptop isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data stays at the ghat.&lt;/strong&gt; Photos, questions and family locations never leave the laptop and phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's free.&lt;/strong&gt; No API keys, no subscription — a laptop the family already has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local languages.&lt;/strong&gt; Bhojpuri and Maithili rarely get first-class support; open models and open notes let the community improve that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone can swap or tune the model&lt;/strong&gt; — one line in &lt;code&gt;config/settings.toml&lt;/code&gt; — or rewrite the notes for their own family's customs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits (honest list)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bhojpuri and Maithili are beta.&lt;/strong&gt; The model may slip into Hindi; Hindi is the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; on a laptop CPU, reading a photo can take tens of seconds, and phones take turns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPS&lt;/strong&gt; near water and crowds is often off by 5–20 m — so the landmark photo and note are always on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a lost-person tracker, and not an authority.&lt;/strong&gt; Official announcements, police, volunteers and elders come first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not field-tested yet with real Gemma output&lt;/strong&gt; — the automated tests use a stand-in model and emulated GPS/compass; the outdoor test is next.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize category
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — Gemma 3 4B reads the notices and writes the explanations and answers, fully offline, on one laptop.&lt;/p&gt;

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

&lt;p&gt;Code (MIT): &lt;strong&gt;&lt;a href="https://github.com/ThePlator/Chhath_Ghat_Guide-" rel="noopener noreferrer"&gt;https://github.com/ThePlator/Chhath_Ghat_Guide-&lt;/a&gt;&lt;/strong&gt; — setup, the design docs (HLD/LLD), and the field-test sheet are in the repo. Checked ritual notes, Bhojpuri/Maithili corrections and sample notice photos (without identifiable people) are especially welcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;छठी मइया सबके मनोकामना पूरा करस।&lt;/em&gt; 🌅&lt;/p&gt;

&lt;p&gt;— Sameer&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>Dawa Samjho — I built my parents an offline app that explains their medicines in Hindi</title>
      <dc:creator>Sameer</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:26:49 +0000</pubDate>
      <link>https://dev.to/theplator/dawa-samjho-i-built-my-parents-an-offline-app-that-explains-their-medicines-in-hindi-1h3k</link>
      <guid>https://dev.to/theplator/dawa-samjho-i-built-my-parents-an-offline-app-that-explains-their-medicines-in-hindi-1h3k</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;p&gt;My parents take several medicines every day. Every strip is printed in tiny English text, and every prescription is in a doctor's handwriting. So I often get the same question, usually with a strip held up close to the light:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"यह गोली किसलिए है?"&lt;/strong&gt; — &lt;em&gt;"What is this tablet for?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a simple question, and they shouldn't have to wait for me to come home to get an answer. Asking the chemist the same thing again feels awkward, and uploading prescriptions to some random app on the internet doesn't feel right either.&lt;/p&gt;

&lt;p&gt;So for "Build for a Friend", I built for the two people I'd most like to help.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Dawa Samjho&lt;/strong&gt; (दवा समझो, "understand your medicine") reads a photo of a medicine strip or prescription and explains every medicine in &lt;strong&gt;simple Hindi&lt;/strong&gt; (or English):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💊 &lt;strong&gt;what it's for&lt;/strong&gt;, in everyday words&lt;/li&gt;
&lt;li&gt;🕗 &lt;strong&gt;when to take it&lt;/strong&gt;, &lt;em&gt;only&lt;/em&gt; as the doctor wrote it&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;general precautions&lt;/strong&gt;, like "tell the doctor if you feel dizzy"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three things mattered more to me than features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It runs entirely on our laptop.&lt;/strong&gt; The AI is Gemma, an open-weight model running locally through Ollama. Prescriptions never leave the house, and it works with the Wi-Fi turned off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never guesses a dose.&lt;/strong&gt; If it can't read something, it says &lt;strong&gt;"साफ़ नहीं पढ़ पाए — डॉक्टर या केमिस्ट से पूछें"&lt;/strong&gt; ("Couldn't read this clearly — ask your doctor or pharmacist") instead of making something up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's built for them, not for me.&lt;/strong&gt; Large text, big buttons, every label in Hindi with English beside it, and a one-click copy button to share the result in the family WhatsApp group.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It only explains what's already written. It never gives medical advice, and every result ends with the same line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;यह सिर्फ समझने के लिए है। हमेशा अपने डॉक्टर की सलाह मानें।&lt;br&gt;
&lt;em&gt;This is for understanding only. Always follow your doctor's advice.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

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

&lt;p&gt;The flow is four steps: choose हिंदी, upload or take a photo, press &lt;strong&gt;समझाओ · Explain&lt;/strong&gt;, and read one card per medicine.&lt;/p&gt;

&lt;p&gt;I tested it on the strips from our own medicine box, then handed the laptop to my parents. They liked it, and what I liked most was watching them do it themselves, without calling me over.&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/ThePlator" rel="noopener noreferrer"&gt;
        ThePlator
      &lt;/a&gt; / &lt;a href="https://github.com/ThePlator/Dawa_Samjho" rel="noopener noreferrer"&gt;
        Dawa_Samjho
      &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;दवा समझो · Dawa Samjho&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Understand your medicine.&lt;/strong&gt; Take a photo of a prescription or medicine strip and get each medicine explained in simple Hindi or English. Everything runs on your own laptop, so your health data stays on your machine.&lt;/p&gt;
&lt;p&gt;Built 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;. The "friend" is a parent who takes several medicines a day and can't always read the doctor's handwriting or the English brand names.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Status:&lt;/strong&gt; the core app works (P0). Accuracy on handwritten prescriptions is still being tested, so always check names against the paper.&lt;/p&gt;
&lt;/blockquote&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;Open the app in your browser (it runs locally).&lt;/li&gt;
&lt;li&gt;Pick a language: &lt;strong&gt;हिंदी&lt;/strong&gt; (default) or &lt;strong&gt;English&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Upload a photo of a prescription or medicine strip, or take one with your camera.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;समझाओ · Explain&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Read one card per medicine
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;, exactly as printed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purpose&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&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/ThePlator/Dawa_Samjho" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Once &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; is installed, setup is three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The stack is deliberately small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 (4B)&lt;/strong&gt;, the smallest Gemma that can read images, served by &lt;strong&gt;Ollama&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streamlit&lt;/strong&gt; for the UI, in pure Python&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillow&lt;/strong&gt; for image handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pydantic&lt;/strong&gt; to validate the model's output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It all runs on an ordinary laptop with about 7 GB of RAM, integrated graphics and no GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One photo, one pipeline.&lt;/strong&gt; When you press &lt;em&gt;Explain&lt;/em&gt;, the app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;checks the photo, fixes its rotation and shrinks it to 1024 px,&lt;/li&gt;
&lt;li&gt;makes sure Ollama is running and the model is installed,&lt;/li&gt;
&lt;li&gt;sends the image to Gemma with a strict prompt that asks for &lt;strong&gt;JSON only&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;validates that JSON against a schema, retrying once if it's malformed,&lt;/li&gt;
&lt;li&gt;runs the answer through &lt;strong&gt;safety rules written in code&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;shows one card per medicine.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The biggest lesson: don't trust a model to be honest about itself
&lt;/h3&gt;

&lt;p&gt;My first real test was a sample of doctor's handwriting that said &lt;em&gt;"Cap Risek 20mg"&lt;/em&gt;. Gemma read it as &lt;strong&gt;"Cap Risele"&lt;/strong&gt;, marked it as perfectly readable, and then added that it should be taken &lt;strong&gt;"once" (एक बार)&lt;/strong&gt;. Nothing on the paper said that.&lt;/p&gt;

&lt;p&gt;That changed the project. A prompt that says "never guess" isn't enough for health information, so I moved the safety rules into code, where I can test them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No invented numbers.&lt;/strong&gt; If a strength or dose in the answer doesn't appear in the text the model says it read from the image, the medicine is marked "couldn't read clearly". Hindi digits like १ are normalised first, so they're compared fairly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No invented timings.&lt;/strong&gt; If the text on the paper contains no timing at all (no &lt;code&gt;1-0-1&lt;/code&gt;, no &lt;code&gt;OD&lt;/code&gt;/&lt;code&gt;BD&lt;/code&gt;, no "after food"), any timing the model adds is removed, and the card says &lt;em&gt;"Not written — ask your doctor"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No made-up advice.&lt;/strong&gt; If the model doesn't recognise a medicine, the app shows no purpose and no precautions, rather than generic filler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always checkable.&lt;/strong&gt; "What the app read" is one click away, so you can compare it with the paper, and every result reminds you to match the name against the strip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this is covered by 32 unit tests, including the exact &lt;em&gt;"Risele / once"&lt;/em&gt; case that started it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest limits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Handwriting is hard.&lt;/strong&gt; On cropped samples of doctors' handwriting, Gemma 4B often gets &lt;em&gt;close but wrong&lt;/em&gt;, which is exactly why the code-level checks exist. Printed strips are much easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not instant.&lt;/strong&gt; It takes about 65–90 seconds per photo on my laptop's CPU, and the first photo is slowest while the model loads. That's slow for a demo, but still faster than waiting for me to get home.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For this app, open isn't a nice extra. It's the only way it could exist.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy you can check.&lt;/strong&gt; Prescriptions are some of the most personal documents a family has. With an open-weight model running locally, "your data stays at home" isn't a promise you have to trust: turn off the Wi-Fi and watch it still work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free to run, forever.&lt;/strong&gt; There's no API key, no subscription and no cost per photo. That matters for retired parents, and for every family that could use something like this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It speaks &lt;em&gt;our&lt;/em&gt; language.&lt;/strong&gt; Big apps are built for English first. Because the model and the code are open, I could tune the prompt for simple, spoken Hindi, and anyone can adapt it for Bhojpuri, Bengali, Tamil or Marathi without asking anyone's permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety anyone can inspect.&lt;/strong&gt; Every rule that stops the app from inventing a dose is in a file anyone can read, test and improve. For health information, being able to &lt;em&gt;check&lt;/em&gt; a system matters as much as how clever it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My parents don't know what an open-weight model is. They just know that when they hold up a strip and ask "यह गोली किसलिए है?", something in the house now answers them in their own language, and honestly.&lt;/p&gt;

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