Kaagaz 以本機 Gemma 4 E4B 讀取文件並製作符合申請要求的上載檔案
An offline assistant that reads your documents with Gemma 4 on your own GPU, then builds upload-ready files to each portal's exact rules.
Anand Kashyap 建立 Kaagaz 離線助理,透過 Ollama 在本機運行 Gemma 4 E4B,辨識文件、解析申請平台要求並準備符合規格的上載檔案。
What I Built
If you've applied for a scholarship, an admission or an exam in India, you know this screen:
Photograph: JPG, 3.5 cm × 4.5 cm, white background, 20–50 KB. Signature: JPG, 10–20 KB. Class 12 marksheet: PDF, front and back in one file, max 200 KB. Income certificate: issued on or after 01-04-2026, PDF, not more than 0.3 MB.
Every portal wants the same documents in a slightly different shape. So you dig through Downloads and WhatsApp forwards called IMG-20240612-WA0007.jpg, upload your marksheet to some random online compressor, get "file size too small" on the signature, and find out on the last day that your income certificate is from March.
Kaagaz (कागज़, "paper") fixes that, and it does it without your documents ever leaving your laptop.
It reads your folders and labels every document (Class 12 marksheet, income certificate, passport photo, signature, 28 types in all) using Gemma 4 E4B running locally through Ollama. It reads the document itself, not the filename.
You paste the portal's "documents to upload" section. Gemma turns it into an editable checklist.
Kaagaz picks the right file for each row and tells you before you start the form what's missing or expired.
Plain Python builds the files: converts, face-crops, compresses to the exact KB range, renames to the portal's pattern.
Then it re-opens every output and measures it. PASS / WARN / FAIL, with the real numbers. A file that fails is never saved, so you can't upload it by mistake.
The rule I built everything around:
AI decides what. Code does how. Code verifies.
The model is good at reading a messy certificate or a wall of portal instructions. It is not the thing I want deciding whether a JPEG is 49 KB or 51 KB.
Who it's for: students and families filling Indian government and college forms, especially people who shouldn't have to upload their Aadhaar or marksheets to a stranger's website to make a file smaller.
Demo
Kaagaz runs on your own machine, so there's no hosted version to click. Here's what it looks like, using specimen documents I generated for an invented student, Riya Sharma:
That match screen is the moment the whole project is for: the income certificate was issued on 12 March, the form wants 1 April or later, so it's flagged EXPIRED now, not at document verification three weeks from now.
📖 Full guide (setup, every screen, troubleshooting): kaagaz-docs.vercel.app
Code
kashyapanand21
/
kaagaz
An offline assistant that reads your documents with Gemma 4 on your own GPU, then builds upload-ready files to each portal's exact rules, and measures every one.
Kaagaz 📄🦥
Your documents, ready for any form. Offline.
Every scholarship, admission and exam portal in India wants the same documents in a different format: "photo 3.5 × 4.5 cm, JPG, 20–50 KB", "marksheet PDF under 200 KB", "income certificate issued on or after 01 April". Kaagaz reads your folders, recognises each document with Gemma 4 E4B running locally through Ollama, and when you paste a portal's upload instructions it builds a folder of files converted, cropped, compressed and renamed to the exact rules. Then it re-opens and measures every output, and tells you what's missing or expired before you start the form.
AI decides what. Code does how. Code verifies.
Nothing leaves your laptop. No uploads, no online compressors, no cloud API.
📖 Guide: kaagaz guide page (setup, how to use, results, troubleshooting)
Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 3 ("Slow and…
Python 3.12, a Typer CLI and a Streamlit app over one package. 71 tests, none of which need a GPU.
How I Built It
The stack, and the hardware surprise
Gemma 4 E4B via Ollama, Pydantic for every model schema, Pillow and PyMuPDF for the file work,
OpenCV's Haar cascades for face cropping, SQLite for the library, Streamlit for the UI.
The plan assumed an 8 GB GPU with Gemma 4 12B as a fallback for hard documents. On Day 1,
nvidia-smi wasn't even recognised: my RTX 4050 Laptop GPU had no driver installed, and
showed up as "Microsoft Basic Display Adapter". Once it was installed, it turned out to have
6 GB. 12B doesn't fit. So Kaagaz is E4B-only, and anything it's unsure about goes to a
review queue for a human instead of a bigger model. In hindsight that's the more honest design
anyway.
One door to the model
Every Gemma call goes through one function with the same settings:
-
think=False: Gemma 4 thinks by default in Ollama. On my first test it spent ~260 hidden tokens to produce a 15-token answer. Turning it off saved about 5 seconds per document. -
num_ctx=8192so the whole model stays on the 6 GB GPU. - Temperature 0 and a JSON schema from Pydantic, so the output is structured and repeatable.
- Ollama down → the run pauses. Bad JSON → that one document goes to review. Nothing crashes, and a scan resumes exactly where it stopped. Downscaling page images to 1,024 px brought labelling down to a median of 2.1 s per document (p95 3.0 s). Building a whole 6-file packet takes 0.3 s.
The bug I'd warn everyone about: optional fields
The first version of the requirement parser got 22 of 27 limits right on my test text. Every
single miss was an omission: the photo's 20 KB minimum, the 3.5 × 4.5 cm size, the signature's
10 KB minimum. Nothing was wrong; things were just… absent.
The cause: fields with defaults are optional in the JSON schema, and Ollama's constrained
decoding lets a small model skip optional keys. So it did. The fix is one line: make every key
required in the schema sent to the model (null is still allowed).
class ParsedItem(BaseModel):
# Every key REQUIRED in the schema sent to Ollama (null still allowed),
# so the model has to consider each field instead of skipping it.
model_config = ConfigDict(
json_schema_extra=lambda s, _: s.update(required=list(s["properties"])))
That got it to 27 of 27, at the cost of ~5 seconds per checklist, which is fine for something
you run once per form. Two days later I found the exact same bug in document extraction: the
model had left the person's name and year empty on 13 of 13 documents. Now every model-facing
schema in Kaagaz makes all keys required.
Code checks the model in both directions
I don't trust the model's confidence score to catch its own mistakes, so the parser has to
quote the sentence each checklist row came from, and then code checks it:
- Grounding: every number the model returned must appear in that sentence (allowing for 0.3 MB ↔ 300 KB and 35 mm ↔ 3.5 cm). An invented limit gets flagged.
- Coverage: every number-with-a-unit in the sentence must be used by some field. A missed limit gets flagged. Flagged cells show up highlighted in the editable checklist. Only numbers with units count, so "Class 12" and "01-04-2026" are never mistaken for size limits.
The model reads, code converts
Indian documents print dates day-first: 14-06-2022. I'd typed the date fields as date, and
constrained decoding squeezed those digits into ISO slots: "1406-06-20". Pydantic happily
accepted it as a valid date in the year 1406. That's the worst kind of bug: silent.
Now Gemma copies dates exactly as printed, and a small tested parser converts them
(day-first, two-digit years, month names, even Devanagari digits like ०५-०८-२०२५). Sizes work
the same way. The model never does arithmetic.
It also caught Gemma copying a date of birth as a marksheet's issue date (2006, on a 2024
marksheet). Code now cross-checks the issue date against the printed year and drops the date if
they disagree. Two model outputs checking each other.
EXPIRED only from facts
Kaagaz only calls a document EXPIRED when it can prove it: a printed expiry date, or an issue
date before the portal's cutoff. Rules of thumb like "income certificates are valid about a year"
can only produce CHECK DATE, clearly labelled as an estimate. I'd rather ask you than tell you
your certificate is expired based on a guess.
Privacy is enforced in code, not promised in a README
- The model host must be
127.0.0.1; a remote host makes Kaagaz refuse to start. - Aadhaar, PAN and account numbers are stored as the last 4 digits only, enforced by a validator and a scrubber that runs on every saved field, including the model's raw output.
- Your original files are never changed. Outputs go to a separate Packets folder.
- Streamlit sends anonymous usage statistics by default. I turned that off, and the app only
listens on
127.0.0.1. The UI started from AI-generated mockups that loaded Tailwind, Google Fonts and images from the internet; I stripped all of it so the app looks the same with Wi-Fi off. ### Slow and steady, literally
Kaagaz is built to be patient. Scanning is two-phase: registering files takes seconds, and
labelling (~2 s each) runs after. Every document is committed as it finishes, so you can close
the laptop halfway through 400 files and it picks up where it stopped. That's where the sloth
came from: it reads your folders slowly, privately, and doesn't lose its place.
The last-night surprise
On the final night, labelling with Wi-Fi off started failing:
CUDA error: shared object initialization failed. It looked like Kaagaz needed the internet.
It didn't. I ran nine cold loads with ollama stop / ollama run: two worked, seven crashed,
with Wi-Fi on and off. Ollama's own log showed the crash happened in its model warm-up step,
inside the CUDA 13 backend, and the Ollama install folder had been rewritten the afternoon
before: the Windows app had auto-updated itself mid-challenge.
The workaround was forcing the CUDA 12 backend (setx OLLAMA_LLM_LIBRARY cuda_v12). RESULT.
Kaagaz now retries a crashed runner three times with longer waits, then pauses with a calm
message instead of Windows' "this could allow a malicious user to gain control" text. My lesson:
during a deadline, freeze every moving part of a local AI stack, not just your Python packages.
What I didn't get to
I'm keeping this honest. My dev set is 13 specimen documents I made myself: Gemma plus the
code checks handled 13/13 correctly (11 labelled, 2 sent to review with the right reason,
including both Hindi domicile certificates). That's a smoke test, not a benchmark. I didn't run
the planned held-out eval on 30 real documents, or test the parser on real portal texts. Hindi
names don't yet match an English profile name, and front/back photos aren't merged
automatically. All of it is in the devlog,
including the failures.
Why Does Open Innovation Matter?
The documents in a scholarship application are some of the most sensitive things a student owns:
ID numbers, family income, caste certificates, bank details. Today the "free" way to shrink them
to a portal's KB limit is to upload them to a website you know nothing about.
Open models change that. Gemma 4 running on a mid-range laptop GPU is good enough to read a
Hindi domicile certificate or a page of portal rules, so the whole job can happen on the
student's own machine. And because Kaagaz is open source, anyone can check the claim that
nothing leaves the laptop: the localhost check, the ID scrubber and the Streamlit settings are
all right there in the repo. "Trust me" isn't good enough for documents like these. Readable
code is.
It also matters that it's adaptable. Every state and every portal has its own rules; an open
project can grow those one pull request at a time.
My Agent Session
I built Kaagaz with Claude as a pair programmer, in one long conversation over four days. I
asked it to work like a developer teaching me: explain the reasoning for each piece, give the
code in small parts, and let me run the tests before moving on. I typed and ran everything on my
own laptop, and pasted back every output, error and screenshot.
The most useful moments weren't the code, they were the pushback: refusing to treat a CUDA crash
as a document problem, insisting the 13-document result be reported as a smoke test, catching
that AI-generated mockups had invented features and loaded fonts from the internet, and
reading Ollama's log instead of guessing about Wi-Fi. When a paste went wrong (twice I indented a
database write into an if block), the privacy test caught it before it could be committed.
The Lazy Payoff
I didn't formally time a real application. What I can measure: once your folders are labelled,
going from pasted portal rules to a folder of verified, renamed files takes about 15 seconds of
Gemma reading the rules plus 0.3 seconds of building. Doing the same by hand means finding six
files, converting and compressing each one by trial and error, renaming them, and hoping nothing
expired. For me that's usually the better part of an hour per form.
Prize Categories
Gemma: Kaagaz uses Gemma 4 E4B, locally through Ollama, for its two reading jobs: labelling
documents from a page image (English and Hindi), and turning portal instructions into a
structured checklist. Both run with JSON schemas, think=False, an 8k context on a 6 GB GPU, and
code that checks every answer.
來源:DEV Community · dev.to



