---
title: PageParse
emoji: π
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
---
# PageParse
**Any handwritten page β clean structured data. On CPU. Fully offline.**
PageParse reads a scanned or photographed page of handwritten notes entirely on
your own device and turns it into structured, schema-validated records in a local
database β no GPU, no CUDA, no cloud. Point it at a handwritten page; it cleans the
image, recognises the ink on CPU, maps the messy text to a defined JSON schema, and
saves it to SQLite. It keeps working with the Wi-Fi switched off.
> Built for **The CPU-First Hackathon** β _"Build AI that runs anywhere."_
---
## Table of Contents
- [The Idea](#the-idea)
- [Why It Matters](#why-it-matters)
- [How It Works](#how-it-works)
- [Architecture](#architecture)
- [Model & Runtime (CPU-first)](#model--runtime-cpu-first)
- [Output Schema](#output-schema)
- [Data Model (SQLite)](#data-model-sqlite)
- [Tech Stack](#tech-stack)
- [Project Structure](#project-structure)
- [Getting Started](#getting-started)
- [Usage](#usage)
- [Offline-First by Design](#offline-first-by-design)
- [Quality Gates (CI/CD)](#quality-gates-cicd)
- [Judging Criteria Mapping](#judging-criteria-mapping)
- [Roadmap](#roadmap)
- [Team & Work Division](#team--work-division)
- [Contributing](#contributing)
- [License](#license)
---
## The Idea
People still write on paper β to-do lists, planners, field surveys, recipe cards,
clinic notes. That ink almost never makes it into a system you can search, sort,
or query. **PageParse bridges paper and database, entirely on-device.**
Drop in a scan or photo of a handwritten page. PageParse:
1. **reads** the handwriting on CPU,
2. **maps** it to a defined, relational schema using a small local language model, and
3. **stores** clean structured rows in SQLite you can query and export β
all while completely air-gapped.
The reference build is anchored to one note type β a **handwritten to-do / planner
page** β because the schema is tight and objectively measurable. The same engine
generalises to survey forms, recipe cards, and meeting notes with only a new schema
and grammar.
---
## Why It Matters
GPUs are scarce, costly, and online. Most computing isn't. PageParse is a
demonstration that **the CPU is enough**, and that a genuinely useful app keeps
working when the network doesn't β in clinics, fields, classrooms, and anywhere
connectivity is unreliable.
It also tackles the hackathon's core mission head-on: handwriting is about as
**unstructured** as input gets, and PageParse maps it to a strict, **structured**
schema. Handwriting recognition is a hard, real problem β and that difficulty is
exactly what makes the result worth scoring.
---
## How It Works
PageParse follows the required **Ingestion β Processing β Transformation β Storage**
workflow, with every inference step on CPU and offline.
| Stage | What happens | Runs on |
| ----- | ------------ | ------- |
| **1. Ingestion** | Load a handwritten page (JPG / PNG / PDF). | local |
| **2. Processing** | OpenCV cleanup: grayscale, deskew, adaptive threshold, denoise. | CPU |
| **3. Recognition** | On-device OCR converts ink β raw text (handwriting tier + printed fallback). | CPU |
| **4. Transformation** | A small language model maps raw text β JSON, grammar-constrained to the schema. | CPU |
| **5. Storage** | Validated rows persisted to SQLite; optional local semantic search. | local |
---
## Architecture
```
ββββββββββββββββββββββββββββββββββββββββββββββββ
β PageParse β
β (100% on-device, CPU) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
scan / photo OpenCV ONNX / Tesseract llama.cpp + GBNF SQLite
JPG Β· PNG Β· PDF ββββββββββββ ββββββββββββββββββ ββββββββββββββββββ ββββββββββββ
ββββββββββββββββΆβ IngestionββββββββΆβ Preprocessing ββββΆβ OCR (on CPU) ββββΆβ TransformββββΆβ Storage β
β β clean β deskewΒ·binarizeβ β TrOCR / Surya /β β SLMβJSON β β + vec β
ββββββββββββ denoiseββββββββββββββββββ β Tesseract β β validatedβ ββββββββββββ
ββββββββββββββββββ ββββββββββββββββββ
β
βββββββββββΌββββββββββ
β query Β· search β
β export Β· review β
βββββββββββββββββββββ
```
---
## Model & Runtime (CPU-first)
Declared explicitly per the rules. **No GPU or CUDA anywhere in the inference path.**
All weights are fetched once and cached locally, so runtime needs no network.
| Stage | Model | Runtime | Precision |
| ------------------- | --------------------------------------- | ---------------------- | --------- |
| Handwriting OCR | TrOCR-small (handwritten) / Surya OCR 2 | ONNX Runtime (CPU EP) | INT8 |
| Printed fallback | Tesseract LSTM | Tesseract (CPU) | β |
| Text β schema (SLM) | Qwen2.5-1.5B-Instruct | llama.cpp | Q4_K_M |
| Semantic search | all-MiniLM-L6-v2 | ONNX Runtime (CPU EP) | INT8 |
Schema validity is **guaranteed** by **llama.cpp GBNF grammar-constrained
decoding**: the model is structurally prevented from emitting anything that is not
valid against the PageParse schema, then re-validated with Pydantic.
---
## Output Schema
The structured target that unstructured handwriting is mapped to:
```json
{
"source_page": "string (filename)",
"captured_date": "YYYY-MM-DD",
"tasks": [
{
"task": "string",
"due_date": "YYYY-MM-DD | null",
"priority": "high | medium | low",
"category": "string | null",
"status": "todo | done",
"ocr_confidence": 0.0
}
]
}
```
---
## Data Model (SQLite)
```sql
CREATE TABLE pages (
id INTEGER PRIMARY KEY,
filename TEXT,
captured_date TEXT,
raw_ocr_text TEXT,
created_at TEXT
);
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
page_id INTEGER REFERENCES pages(id),
task TEXT NOT NULL,
due_date TEXT,
priority TEXT CHECK (priority IN ('high', 'medium', 'low')),
category TEXT,
status TEXT DEFAULT 'todo',
ocr_confidence REAL
);
```
---
## Tech Stack
**Language & core**
- Python 3.11
**Computer vision / OCR**
- OpenCV β image preprocessing
- ONNX Runtime (CPU EP) β TrOCR / Surya handwriting OCR
- Tesseract β printed-text fallback
**AI / structuring**
- llama.cpp (`llama-cpp-python`) β Qwen2.5-1.5B Q4_K_M GGUF inference
- GBNF grammar β guaranteed-valid JSON
- Pydantic β schema validation
**Storage & retrieval**
- SQLite β structured persistence
- sqlite-vec + all-MiniLM-L6-v2 (ONNX) β optional semantic search
**Interface**
- Typer β CLI (primary)
- FastAPI + PWA β web interface
**Observability**
- psutil β live CPU & memory telemetry
**Tooling & quality**
- Ruff, mypy, Bandit, pip-audit, detect-secrets, pytest, gitlint, yamllint,
markdownlint, REUSE
- pre-commit, GitLab CI (self-hosted Docker runner)
**License**
- AGPL-3.0-or-later
---
## Project Structure
```
pageparse/
βββ src/
β βββ pageparse/
β βββ __init__.py
β βββ ingest.py # load image / PDF input
β βββ preprocess.py # OpenCV cleanup pipeline
β βββ ocr/
β β βββ __init__.py
β β βββ handwriting.py # TrOCR / Surya (ONNX)
β β βββ printed.py # Tesseract fallback
β βββ extract.py # SLM + GBNF β JSON
β βββ schema.py # Pydantic models
β βββ store.py # SQLite persistence
β βββ search.py # optional semantic search
β βββ telemetry.py # psutil CPU/RAM panel
β βββ config.py # paths, model settings, airgap flag
β βββ cli.py # Typer CLI entry point
β βββ web.py # FastAPI + PWA
βββ grammars/
β βββ task.gbnf # schema-constraining grammar
βββ models/ # bundled GGUF / ONNX weights (gitignored)
βββ scripts/
β βββ fetch_models.py # one-time online model download
β βββ init_db.py # initialise SQLite
βββ samples/ # real handwritten demo pages
βββ tests/
β βββ test_preprocess.py
β βββ test_extract.py
β βββ test_schema.py
β βββ test_store.py
βββ web/
β βββ static/ # PWA assets, service worker
β βββ templates/
βββ docs/
β βββ architecture.md
βββ .gitlab-ci.yml
βββ .pre-commit-config.yaml
βββ .gitignore
βββ .secrets.baseline
βββ pyproject.toml
βββ CONTRIBUTING.md
βββ CHANGELOG.md
βββ LICENSE # AGPL-3.0
βββ README.md
```
---
## Getting Started
### Prerequisites
- Python 3.11+
- Tesseract OCR β `apt install tesseract-ocr` / `brew install tesseract`
- ~3 GB free disk for bundled models
### Installation
```bash
# Clone
git clone https://code.swecha.org/centurions/pageparse.git
cd pageparse
# Environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install (with dev tooling)
pip install -e ".[dev]"
# Fetch models ONCE while online β cached locally thereafter
python scripts/fetch_models.py
# Initialise the local database
python scripts/init_db.py
```
After `fetch_models.py` runs once, **disconnect from the network entirely** β
everything below works offline.
---
## Usage
### CLI
```bash
# Process a single handwritten page
pageparse process samples/todo_page_01.jpg
# Process a whole folder
pageparse process samples/ --batch
# Force air-gapped mode (any outbound call fails loudly)
pageparse process samples/todo_page_01.jpg --airgap
# Query and search
pageparse list --priority high
pageparse search "supplier"
# Export
pageparse export --format json > out.json
```
### Web (PWA)
```bash
pageparse serve # http://localhost:8000
```
Upload a page, watch the **queued β processing β saved** status, and view the
structured result. Installable as a PWA for offline use.
---
## Offline-First by Design
The core feature works with **no cloud calls**, and the demo proves it:
- **Air-gap toggle** β a UI switch and `--airgap` flag assert zero network access;
any accidental outbound call fails loudly rather than silently.
- **Status badge** β every page shows `queued β processing β saved`, making caching
and graceful handling observable.
- **Graceful degradation** β low-confidence OCR lines are flagged for review
instead of crashing; pages queue and process as the CPU frees up.
- **Live resource panel** β a psutil readout of CPU % and RAM during inference, so
the footprint is measured, not guessed.
**Demo procedure:** enable airplane mode / pull the Ethernet, then run a full
ingestion β extraction β storage cycle on a real handwritten page.
---
## Quality Gates (CI/CD)
All checks run locally via **pre-commit** and in CI on a **self-hosted GitLab
runner** (Docker executor). These are real checks β no stub jobs, no `exit 0`.
| # | Check | Tool |
| -- | ------------------- | ---------------- |
| 1 | Lint | Ruff |
| 2 | Format | Ruff format |
| 3 | Type-check | mypy |
| 4 | Security (SAST) | Bandit |
| 5 | Dependency CVEs | pip-audit |
| 6 | Secret scan | detect-secrets |
| 7 | Unit tests | pytest |
| 8 | Semantic commits | gitlint |
| 9 | CI YAML lint | yamllint |
| 10 | Docs lint | markdownlint |
| 11 | License compliance | REUSE |
| 12 | Smoke build | app boots headless |
```bash
pre-commit run --all-files
pytest
```
Conventional Commits are enforced (`feat:`, `fix:`, `chore:`, β¦).
---
## Judging Criteria Mapping
| Criterion | How PageParse addresses it |
| ---------------------- | ------------------------------------------------------------ |
| **Model performance** | OCR confidence + extraction accuracy reported on real pages. |
| **Resource efficiency**| Live psutil CPU/RAM panel during inference. |
| **Offline resiliency** | Air-gap toggle; full cycle demoed with the network off. |
| **Schema alignment** | GBNF grammar guarantees valid JSON every run. |
| **Graceful handling** | Queued processing + flagged low-confidence lines + caching. |
---
## Roadmap
- [ ] **MVP:** scanned to-do page β validated JSON β SQLite (offline, CPU)
- [ ] Printed-text fallback path
- [ ] Web PWA with air-gap toggle and status badge
- [ ] Live CPU/RAM telemetry panel
- [ ] Semantic search over extracted notes (`sqlite-vec`)
- [ ] Additional note types: survey forms, recipe cards, meeting notes
- [ ] Indic-script handwriting support
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). Branch from `main`, keep commits
Conventional, ensure `pre-commit run --all-files` and `pytest` pass, then open a
merge request. All contributions are licensed under AGPL-3.0-or-later.
---
## License
Licensed under the **GNU Affero General Public License v3.0 or later**
(AGPL-3.0-or-later). See [LICENSE](LICENSE).
AGPL is chosen deliberately: as strong copyleft, it closes the network/SaaS
loophole that plain GPL leaves open, ensuring anyone who runs a modified PageParse
as a network service must also share their source.