Spaces:
Build error
Build error
File size: 15,538 Bytes
8c3e275 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 | ---
title: PageParse
emoji: π
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
---
<!--
SPDX-FileCopyrightText: 2026 Team Centurions
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# 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."_
<p>
<img alt="CPU-first" src="https://img.shields.io/badge/inference-CPU--only-blue">
<img alt="Offline" src="https://img.shields.io/badge/network-offline--first-green">
<img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-orange">
<img alt="Python" src="https://img.shields.io/badge/python-3.11-yellow">
</p>
---
## 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. |