Open-source handwriting analysis from the geometry of writing. Upload a photo of a handwriting page (or capture live with the dual-IMU sensor pen) and get a compact, explainable 20-factor report: letter formation, spacing, baseline, slant, pressure, speed and more. Every score is a real measurement with a published reference range and a crop from your own page as evidence.
Try it without installing anything: the free handwriting check on vahinitech.com runs this analyser. Upload one photo of a handwritten page.
Not a diagnostic tool. For handwriting improvement, education and skill-building only. It makes no medical, psychological or personality claims.
docker compose up -d --build
# open http://localhost:8080That single container serves the app and the analysis APIs on one origin. The first run downloads the OCR models, so give it a few minutes.
Pulled new code or changed a Dockerfile/requirements file? Rebuild from a clean slate so nothing stale (cached layer, old dependency) survives:
docker compose down
docker compose build --no-cache
docker compose up -dUse http://, not https://. The local server speaks plain HTTP; if the
browser autocompletes https://localhost:8080 the connection fails before
the app can even redirect. Type http://localhost:8080 and it lands on the
analyser automatically. For HTTPS in production, put the service behind a
TLS reverse proxy such as nginx or Caddy.
To run without Docker:
pip install -r backend/requirements.txt
python backend/analyser-ocr-server.py
# open http://localhost:8080| Section | Weight | Factors |
|---|---|---|
| Structure | 30% | Letter Formation, Stroke Order, Loop Closure, Line Quality, Size Consistency, Ascender/Descender |
| Spatial | 30% | Baseline, Word Spacing, Letter Spacing, Margins, Line Straightness, Vertical Alignment |
| Dynamics | 20% | Speed, Pressure, Stroke Continuity, Pen Lifts (needs the sensor pen) |
| Style & Readability | 20% | Slant, Legibility, Character Distinction, Overall Neatness |
The overall score is the weighted average of the section scores, 0 to 100.
From a photo, the Dynamics factors are not scored; the headline uses only what
was actually measured. The full report is two pages: the overall score,
handwriting-region coverage and up to three priorities with reference crops
from the writer's own page; then illustrated coaching and ruled practice
space. The hosted anonymous Free report is a five-factor summary served from
the compact API (see backend/API-V2.md). There is no prediction and no
score table in either report.
Start at docs/00-start-here.md: the documents are numbered in the order to read them.
No install needed to judge what the analyser delivers:
- Example report — a complete report walked through page by page, every number generated by the real engine.
- Case studies — three writers (sinking baseline, crowded words, single-zone letters), before and after the prescribed drills, scored by the real engine both times.
- Scoring criteria — all 20 factors with
their formulas, targets and band cut-offs, traced line by line to
backend/scoring.py.
We tried scoring with large language and vision-language models and chose classical computer vision instead. The reason is repeatable accuracy:
- An LLM grades the same page differently across runs, model versions and prompt wordings. A child who re-scans the same page must get the same score, or progress tracking means nothing.
- Vector and embedding similarity gives a fuzzy closeness number, not a measurement. It cannot say why a score moved, and a model update silently reshapes the whole vector space, making old scores incomparable.
- Geometry is auditable. Every factor traces to the exact crop of the page it was measured from, and the reference ranges are printed in the report.
- Generative models can be swayed by what the words say when judging how the words look, and can report things that are not in the image. Geometry cannot.
Where a model is the right tool, reading messy words, we use one assistively: the pluggable OCR backends label what was written and never decide a score. If OCR is unavailable the 20 factors are still measured from geometry alone.
"Deterministic computer vision" is a claim, not a black box. Every factor
traces to a real measurement: character-level stroke width and edge
shape, word-level letter spacing, line-level tilt and height, and a few
page-level blends of the factors below it. Most of this never depends on
reading the words correctly; only loop closure, the ascender/descender
proxy and Character Distinction's confidence term lean on OCR text as a
secondary signal. The full walkthrough, from one stroke up to the whole
page, is in docs/05-computer-vision-algorithms.md.
frontend/
analyser.html the app (loads the packed engine bundle)
src/ browser client source. Edit here, then rebuild.
scripts/core/ packed build (engine.bundle.js) + runtime helpers
styles/, static/ report CSS and printable pages
backend/ Python OCR + 20-factor scoring server and its tests
deployment/ Dockerfile (docker-compose.yml stays at the root)
docs/ numbered reading order, start at 00-start-here.md
tests/ headless Chrome e2e + fixtures
The client ships as one packed file, built from frontend/src/ and not
committed. Build it before serving frontend/ from a checkout (the Docker image
builds it for you):
python frontend/build_bundle.pyRun the tests:
# Python server tests (no heavy paddle/torch install needed)
pip install -r backend/requirements-core.txt
python -m unittest -v \
backend.tests.test_backends_classify \
backend.tests.test_server_pipeline \
backend.tests.test_regression_functional \
backend.tests.test_handwriting_only \
backend.tests.test_layout_filter
# Headless Chrome report checks
npm ci && npx playwright install --with-deps chromium
npm run test:regression:headless
# Full recognition test against a running stack
docker compose up -d --wait
VAHINI_BASE_URL=http://localhost:8080 npm run test:recognitionCI runs all three suites on every push and pull request.
Semantic versioning as written in Umbrella/docs/VERSIONING.md. version in package.json is the single source. A release bumps it, moves the ## Unreleased lines in CHANGELOG.md under ## X.Y.Z - YYYY-MM-DD, and tags vX.Y.Z on main; a new report schema_version is a major bump. The tags v0.1 to v0.7 predate this rule and stay. Consumers (vahini-web, the Android app through the contracts tag) pin a release tag rather than a commit once one exists.
Contributions are welcome, see CONTRIBUTING.md, which includes a short contributor license agreement. Please do not commit real handwriting samples containing personal data; the CI fixtures are synthetic.
Want a feature? Open an issue describing the problem it solves and who it helps. For features specific to your school or coaching centre, or anything you'd rather discuss privately, email info@vahinitech.com.
Copyright (c) 2026 Vahini Technologies. Free software under AGPL-3.0-only, see LICENSE. If you run a modified version as a network service you must offer its users the modified source.
- Patent: the dual-IMU handwriting-motion sensing method on ordinary paper is the subject of Indian Patent No. 584433.
- Trademark: "Vahini" and associated marks are not licensed under AGPL-3.0.
- Third-party components keep their own licenses, see THIRD-PARTY-NOTICES.md and sbom.spdx.json.
As the copyright holder, Vahini Technologies also offers this software under a separate commercial license for organizations that need terms AGPL-3.0 does not give them, most commonly embedding it in a proprietary product or service without publishing their own modified source. The AGPL edition in this repository stays free and unrestricted either way. For commercial license terms, contact info@vahinitech.com.