Skip to content

Repository files navigation

OCR Engine

Extract text from images, documents, and ZIP archives using Mistral OCR.

Upload a single file, a whole folder, or a ZIP archive; preview it in the browser; and download the extracted text as .txt or, for several files, as a .zip. Legacy Office documents are converted to PDF with LibreOffice before extraction.

License: AGPL v3


Contents


Features

  • Single, multiple, folder, and ZIP uploads — drag and drop, or browse.
  • Automatic conversion of .doc, .docx, .ppt, and .pptx to PDF via LibreOffice before extraction.
  • In-browser preview for images, PDFs, and .docx.
  • Download one result as .txt, or several bundled into a .zip.
  • Contact form with reCAPTCHA verification.
  • Safe archive handling — path-traversal (zip-slip) members are refused and expansion is size-capped against zip bombs.
  • Zero-config local run — defaults to SQLite, so no database server is needed to get started.

Tech stack

Layer Technology
Backend Python 3.12, Django 5.2 (LTS), Django REST Framework
Frontend React 19, Vite 7, Tailwind CSS 3
OCR Mistral OCR API
Conversion LibreOffice (headless)
Database SQLite by default; MySQL optional
Serving Gunicorn + WhiteNoise (backend), nginx (frontend)

Quick start

With Docker Compose (recommended)

git clone <repository-url>
cd ocr-engine
cp .env.example .env

Edit .env and set at least:

DJANGO_SECRET_KEY=<generate one, see below>
MISTRAL_API_KEY=<your Mistral API key>

Generate a secret key with:

python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Then:

docker compose up --build

DJANGO_SECRET_KEY and MISTRAL_API_KEY are the two variables Compose requires — it stops with an explanatory message if either is missing, rather than starting a stack that cannot work. Everything else has a working default.

RECAPTCHA_SECRET_KEY is optional: without it the service starts normally and logs a warning, and only the contact form is unusable (it rejects submissions with a clear message). Text extraction is unaffected.

Database migrations run automatically on container start, and the SQLite database plus uploaded media persist in the backend-data volume.

Without Docker

You need Python 3.12+, Node.js 22+, and — only for .doc/.docx/.pptx conversion — LibreOffice on your PATH.

Backend:

cd backend
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp ../.env.example .env            # then set MISTRAL_API_KEY
python manage.py migrate
python manage.py runserver

Frontend, in a second terminal:

cd frontend
npm install
cp ../.env.example .env
npm run dev

DJANGO_DEBUG=True must be set for local development — it is already set in .env.example. With debug off, the app deliberately refuses to start unless real production secrets are present.


Configuration

Every variable below is read by the code. Copy .env.example and fill it in.

Required in production

Variable Description
DJANGO_SECRET_KEY Django signing key. Startup fails without it when debug is off.
MISTRAL_API_KEY Mistral OCR API key. Startup fails without it when debug is off.
DJANGO_ALLOWED_HOSTS Comma-separated hostnames. Required when debug is off; * is rejected.

RECAPTCHA_SECRET_KEY is optional: without it the app starts and logs a warning, and only the contact form stops working.

Core settings

Variable Default Description
DJANGO_DEBUG False Debug mode. Never enable in production.
DJANGO_CORS_ALLOWED_ORIGINS localhost:5173 in debug, else empty Browser origins allowed to call the API.
DJANGO_CORS_ALLOW_CREDENTIALS False Send cookies cross-origin.
DJANGO_CSRF_TRUSTED_ORIGINS (empty) Origins trusted for CSRF.
DJANGO_TIME_ZONE UTC Server time zone.
DJANGO_LOG_LEVEL INFO Root log level.

Database

Variable Default Description
DB_ENGINE (empty → SQLite) e.g. django.db.backends.mysql.
DB_NAME Required when DB_ENGINE is set.
DB_USER, DB_PASSWORD, DB_HOST, DB_PORT (empty) Standard connection details.
SQLITE_PATH backend/db.sqlite3 SQLite file location when DB_ENGINE is empty.
MEDIA_ROOT backend/media Where uploaded media is written.

For MySQL, also pip install -r backend/requirements-mysql.txt.

Frontend (build-time)

Variable Default Description
VITE_API_URL http://localhost:8000 Backend origin only; the /ocr prefix is added by the client.
VITE_RECAPTCHA_SITE_KEY (empty) reCAPTCHA v2 site key. Without it the contact form is disabled with a visible notice.

Vite inlines VITE_* variables at build time, so they must be set when building, not when running.

Limits and tuning

Variable Default Description
MISTRAL_OCR_MODEL mistral-ocr-latest OCR model identifier.
MAX_UPLOAD_SIZE 209715200 (200 MB) Largest accepted upload.
MAX_ARCHIVE_EXTRACTED_SIZE 524288000 (500 MB) Cap on total ZIP expansion.
FILE_UPLOAD_MAX_MEMORY_SIZE 10485760 (10 MB) Above this, uploads spill to a temp file.
DATA_UPLOAD_MAX_MEMORY_SIZE 5242880 (5 MB) Cap on non-file request bodies.

Production hardening

Ignored while DJANGO_DEBUG is on.

Variable Default Description
DJANGO_SECURE_SSL_REDIRECT True Redirect HTTP to HTTPS.
DJANGO_SECURE_HSTS_SECONDS 31536000 HSTS max-age. Set 0 while testing TLS.
DJANGO_USE_X_FORWARDED_PROTO False Trust X-Forwarded-Proto. Only behind a proxy that strips it.

API reference

Base URL: http://localhost:8000. All app routes are mounted under /ocr/. There is no authentication.

POST /ocr/upload/

Extract text from one or more files. Body: multipart/form-data with one or more file parts.

Accepted content types: application/pdf, image/jpeg, image/jpg, image/png, application/msword, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/zip, application/x-zip-compressed.

curl -F "file=@scan.pdf" http://localhost:8000/ocr/upload/

Single file200:

{ "Transcription": "--- Page 1 ---\nExtracted text…" }

Multiple files or a ZIP200, keyed by filename. Files converted to PDF appear under their new .pdf name:

{
  "results": {
    "invoice.pdf": "--- Page 1 ---\n",
    "notes.pdf": "--- Page 1 ---\n"
  }
}

Within a batch or archive, a file that fails individually gets an "[error] …" string as its value rather than aborting the whole request.

Error responses carry {"error": "..."}, except validation failures which return DRF's field-error object:

Status Meaning
400 No file, unsupported content type, or a corrupt/empty archive.
413 Upload exceeds MAX_UPLOAD_SIZE, or the archive expands past MAX_ARCHIVE_EXTRACTED_SIZE.
502 LibreOffice conversion failed, or the OCR provider errored.
503 Server misconfigured — no MISTRAL_API_KEY, or LibreOffice not installed.

POST /ocr/submit_form/

Store a contact-form submission. Body: application/json.

{
  "first_name": "Ada",
  "last_name": "Lovelace",
  "company": "Analytical Engines",
  "email": "ada@example.com",
  "project": ["OCR Engine", "Resume Parser"],
  "message": "Please get in touch.",
  "recaptcha_token": "<token from the reCAPTCHA widget>"
}

All fields are required; project must hold at least one entry and is stored comma-separated.

  • 201{"message": "Form submitted successfully"}
  • 400 → field errors, including a failed or unconfigured reCAPTCHA check.

This endpoint stores personal data. See SECURITY.md.

/admin/

The standard Django admin, exposing stored contact submissions. Create a user with python manage.py createsuperuser. Restrict or remove it in production.


Project structure

ocr-engine/
├── backend/
│   ├── OCRengine/            # Django project (settings, URLs, WSGI/ASGI)
│   ├── appOCR/
│   │   ├── migrations/
│   │   ├── admin.py          # Admin registration for submissions
│   │   ├── exceptions.py     # Domain errors carrying HTTP status codes
│   │   ├── models.py         # OCRAppFormSubmission
│   │   ├── serializers.py    # Request validation
│   │   ├── services.py       # Business logic: conversion, archives, batching
│   │   ├── tests.py          # 26 tests
│   │   ├── urls.py
│   │   ├── utils.py          # LibreOffice + Mistral integration
│   │   └── views.py          # Thin HTTP layer
│   ├── .pylintrc
│   ├── Dockerfile            # Multi-stage, non-root, Gunicorn
│   ├── manage.py
│   ├── requirements.txt
│   ├── requirements-dev.txt
│   └── requirements-mysql.txt
├── frontend/
│   ├── public/
│   ├── src/
│   │   ├── components/       # Presentational components
│   │   ├── constants/        # Shared file and product constants
│   │   ├── hooks/            # useFileManager, useExtraction, useContactForm
│   │   ├── pages/Home.jsx    # Workspace page
│   │   ├── services/         # API client
│   │   └── utils/            # Text rendering and download helpers
│   ├── Dockerfile            # Multi-stage, non-root nginx
│   ├── eslint.config.js
│   ├── nginx.conf
│   └── vite.config.js
├── .github/workflows/ci.yml
├── .env.example
├── docker-compose.yml
├── CONTRIBUTING.md
├── LICENSE
└── SECURITY.md

appOCR and OCRengine do not follow PEP 8 module naming. The names are recorded in the migration history and the appocr_* table names, so renaming them would break existing deployments; the lint rule is relaxed in backend/.pylintrc instead.


Development

See CONTRIBUTING.md for the full workflow and the exact commands CI runs. In short:

# Backend, from backend/ with DJANGO_DEBUG=1
pylint OCRengine appOCR manage.py
python manage.py test

# Frontend, from frontend/
npm run lint
npm test
npm run build

Deployment

Both Dockerfiles are multi-stage, run as a non-root user, and use production servers — Gunicorn for the backend, nginx for the frontend. Neither runs a development server.

The backend image collects static files at build time and applies migrations on container start. The frontend image serves the built bundle on port 8080 inside the container (mapped to 5173 by Compose).

Work through the safe-deployment checklist before exposing an instance publicly.


Limitations

Known and deliberate, so you can plan around them:

  • No authentication or rate limiting. Every endpoint is public, and each upload costs a paid Mistral API call. Put an authenticating, rate-limiting proxy in front of any public deployment.
  • Extraction is synchronous. A large document holds an HTTP connection open for its whole duration; Gunicorn's timeout is set to 300s. There is no job queue or progress endpoint — the frontend's progress bar reflects files completed in a client-side loop, not server-side progress.
  • Multi-file uploads are processed one request at a time by the frontend, sequentially, so a large batch is slow.
  • Nested archives are not unpacked. A ZIP inside a ZIP is skipped.
  • .doc/.docx/.pptx need LibreOffice. Without it those uploads fail with a 503; every other format still works.
  • Preview support is narrower than extraction support. Images, PDFs, and .docx preview in-browser; .doc and .pptx extract but show a placeholder.
  • Uploaded files are sent to a third-party API and are not retained locally after processing.
  • No retention policy for contact-form submissions.
  • The frontend's declared webkitdirectory folder upload is Chromium-only; other browsers fall back to file selection.

Security and privacy

  • Report vulnerabilities privately — see SECURITY.md.
  • The contact form stores personal data, and uploaded documents are sent to Mistral. Both are documented in SECURITY.md; if you run a public instance, tell your users.

Licence

Licensed under the GNU Affero General Public License v3.0 — see LICENSE.

The AGPL's network clause matters here: if you run a modified version of this software as a network service, you must offer its source to users of that service.

About

OCR engine built to convert scanned documents, invoices, forms, and images into editable and searchable digital text with high accuracy and efficiency.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages