diff --git a/.example.env b/.example.env deleted file mode 100644 index 22d7cd2..0000000 --- a/.example.env +++ /dev/null @@ -1,46 +0,0 @@ -###################################################################### -# Example Environment Configuration (DO NOT PUT REAL SECRETS) # -# Copy to `.env` (git-ignored) and replace placeholder values. # -# # -# NEVER commit real API keys or tokens. # -# Loaded automatically via python-dotenv. # -# Only populate the sections relevant to the providers you use. # -###################################################################### - -############################# -# Azure OpenAI / OpenAI API # -############################# -# Base URL of your Azure OpenAI resource (Ends with .openai.azure.com/) -AZURE_API_BASE=https://.openai.azure.com/ -# One of the two keys under Azure OpenAI: Portal > Resource > Develop > Keys & Endpoint -AZURE_OPENAI_API_KEY= -# API version (update only if you intentionally target a newer preview/stable) -AZURE_API_VERSION=2025-04-01-preview - -# Direct OpenAI key (use only if calling OpenAI instead of Azure deployment routing) -OPENAI_API_KEY= - -# Mistral key from https://console.mistral.ai (Workspace > API keys) -MISTRAL_API_KEY= - -############################# -# Local models through vLLM and HuggingFace # -############################# -# Port where your local vLLM server will be exposed. Any free port larger than 1024 -# and less than 65535 should work. -LOCAL_VLLM_PORT=9000 - -# Hugging Face access token (Settings > Access Tokens). Needed for private/gated models -HF_TOKEN= - -####################################### -# Azure Document Intelligence, if you are using Azure OCR -####################################### -# Endpoint of your Document Intelligence resource -AZURE_DI_ENDPOINT=https://.cognitiveservices.azure.com -# One of the two keys under that resource (Keys & Endpoint) -AZURE_DOC_KEY= - -# Vertex AI settings -# Location of your Vertex AI resources, e.g. us-central1, us-east5, global -VERTEX_AI_LOCATION= \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..476bb3c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,62 @@ +name: docs + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Pixi + uses: prefix-dev/setup-pixi@v0.9.3 + with: + pixi-version: v0.66.0 + cache: true + + - name: Build documentation + run: pixi run docs-build + + - name: Configure GitHub Pages + if: github.event_name != 'pull_request' + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v4 + with: + path: docs/_build/html + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..da3f772 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,73 @@ +name: publish + +on: + release: + types: + - published + +permissions: + contents: read + +concurrency: + group: publish-${{ github.event.release.tag_name }} + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Pixi + uses: prefix-dev/setup-pixi@v0.9.3 + with: + pixi-version: v0.66.0 + cache: true + + - name: Verify release tag matches package version + run: | + python - <<'PY' + import os + import tomllib + from pathlib import Path + + pyproject = tomllib.loads(Path("pyproject.toml").read_text()) + version = pyproject["project"]["version"] + tag = os.environ["GITHUB_REF_NAME"] + expected = f"v{version}" + if tag != expected: + raise SystemExit( + f"Release tag {tag!r} does not match project.version {expected!r}." + ) + PY + + - name: Build and validate release artifacts + run: pixi run package-check + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/churro-ocr + permissions: + contents: read + id-token: write + + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 313ed49..b5e4439 100644 --- a/.gitignore +++ b/.gitignore @@ -1,173 +1,25 @@ -# Byte-compiled / optimized / DLL files +# Python caches __pycache__/ *.py[cod] -*$py.class - -# C extensions *.so +*.pyd +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.litellm_cache/ +.cache/ -# Distribution / packaging -.Python +# Local environments and build artifacts +.pixi/ +.coverage build/ -develop-eggs/ dist/ -downloads/ -eggs/ -.eggs/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation docs/_build/ +*.egg-info/ -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -.vscode - -# pixi environments -.pixi/ - -# Runtime artifacts +# Local data and logs workdir/ workdir -prompt_logs.jsonl -.litellm_cache/ -.diskcache/ \ No newline at end of file +debug.log +debug_logs* +.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..39b888c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +minimum_pre_commit_version: 4.0.0 +default_install_hook_types: + - pre-commit + - pre-push + +repos: + - repo: local + hooks: + - id: format + name: pixi format + entry: pixi run format + language: system + pass_filenames: false + stages: + - pre-commit + files: ^(src/|tests/|pyproject\.toml|pixi\.lock|pytest\.ini|ruff\.toml|ty\.toml) + + - id: lint + name: pixi lint + entry: pixi run lint + language: system + pass_filenames: false + stages: + - pre-commit + files: ^(src/|tests/|pyproject\.toml|pixi\.lock|pytest\.ini|ruff\.toml|ty\.toml) + + - id: typecheck + name: pixi typecheck + entry: pixi run typecheck + language: system + pass_filenames: false + stages: + - pre-commit + files: ^(src/|tests/|pyproject\.toml|pixi\.lock|pytest\.ini|ruff\.toml|ty\.toml) + + - id: test + name: pixi test + entry: pixi run test + language: system + pass_filenames: false + stages: + - pre-push + files: ^(src/|tests/|pyproject\.toml|pixi\.lock|pytest\.ini|ruff\.toml|ty\.toml) + + - id: docs-build + name: pixi docs-build + entry: pixi run docs-build + language: system + pass_filenames: false + stages: + - manual + + - id: package-check + name: pixi package-check + entry: pixi run package-check + language: system + pass_filenames: false + stages: + - manual diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ce4df58 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS + +This repo is `churro-ocr`, a Python 3.12 OCR and page-detection toolkit for historical documents. + +## Repo Layout + +- `src/churro_ocr/`: library code and CLI entrypoints +- `tests/`: pytest suite and test assets +- `docs/`: Sphinx documentation source +- `scripts/` and `tooling/`: packaging, benchmarking, and evaluation helpers + +## Preferred Workflow + +Use Pixi from the repo root: + +```bash +pixi install +pixi run format +pixi run lint +pixi run typecheck +pixi run test +pixi run docs-build +pixi run package-check +``` + +## Guardrails + +- Prefer the Pixi tasks above over ad hoc commands. +- Keep library changes in `src/churro_ocr/` and add or update tests in `tests/`. +- Do not enable live integration tests unless the task explicitly calls for them and the required credentials are available. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5275805 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +include LICENSE +include docs/pypi.md +include pyproject.toml +graft src/churro_ocr +prune scripts +prune tests +prune tooling +global-exclude __pycache__ +global-exclude *.py[cod] diff --git a/README.md b/README.md index 3dcb8af..f133067 100644 --- a/README.md +++ b/README.md @@ -1,291 +1,37 @@ -

- CHURRO Logo -

CHURRO: Making History Readable with an Open-Weight Large Vision-Language Model for High-Accuracy, Low-Cost Historical Text Recognition

-

- Model - Dataset - Paper - GitHub Stars -

-

- -

- Handwritten and printed text recognition across 22 centuries and 46 language clusters, including historical and dead languages. -

- -

- Cost vs Performance comparison showing CHURRO's accuracy advantage at significantly lower cost -
- Cost vs. accuracy: CHURRO (3B) achieves higher accuracy than much larger commercial and open-weight VLMs while being substantially cheaper. -

- ---- -## Table of Contents -1. [Overview](#overview) -2. [Quick Start](#quick-start) -3. [Installing the Full Package](#installing-the-full-package) - - [System Packages](#system-packages) - - [Docker (recommended for local models)](#docker-recommended-for-local-models) - - [Environment Setup](#environment-setup) - - [Configure Providers](#configure-providers) -4. [CLI Workflows](#cli-workflows) - - [Inference](#inference) - - [Preprocess PDFs and Images](#preprocess-pdfs-and-images) - - [Benchmark on CHURRO-DS](#benchmark-on-churro-ds) - - [LLM Improver](#llm-improver) - - [Backup Engines](#backup-engines) - - [Local vLLM Container Notes](#local-vllm-container-notes) -5. [Adding a New OCR System](#adding-a-new-ocr-system) -6. [HistoricalDocument XML](#historicaldocument-xml) - - [Generate HistoricalDocument XML](#generate-historicaldocument-xml) -7. [Citation](#citation) -8. [License](#license) - ---- - -## Overview -**CHURRO** is a 3B-parameter open-weight vision-language model (VLM) for historical document transcription. It is trained on **CHURRO-DS**, a curated dataset of ~100K pages from 155 historical collections spanning 22 centuries and 46 language clusters. - -On the CHURRO-DS test set, CHURRO delivers **15.5× lower cost than Gemini 2.5 Pro while exceeding its accuracy**. - -## Quick Start - -Want a minimal demo? The following will install `transformers` and `torch` only: -```bash -git clone https://github.com/stanford-oval/churro.git -cd churro -curl -fsSL https://pixi.sh/install.sh | bash -pixi shell -e minimal -``` - -Then run: -```bash -python churro_transformers_infer.py tests/churro_dataset_sample_1.jpeg --max-new-tokens 40 -``` - -Expected output begins with: - -```xml - - - German - ltr - [!WARNING] -> This codebase has been tested on Ubuntu 20.04+. Using other operating systems may require tinkering with and troubleshooting system dependencies. - -### System Packages -```bash -sudo apt-get update && sudo apt-get install -y \ - libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev libfreetype6-dev \ - liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk libharfbuzz-dev \ - libfribidi-dev libxcb1-dev -``` - -### Docker (recommended for local models) -- Install Docker: https://docs.docker.com/engine/install/ -- GPU users: add the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html). -- CPU-only machines can still run local models, but expect significantly slower throughput. - -### Environment Setup -We use [Pixi](https://pixi.sh/) to manage Python environments and dependencies. If you are familiar with [Conda](https://docs.conda.io/), you can think of Pixi as a much faster alternative. The following commands set up a Pixi shell with all required packages. Make sure the environment is active before running any Python code. -```bash -git clone https://github.com/stanford-oval/churro.git -cd churro -curl -fsSL https://pixi.sh/install.sh | bash -pixi shell # create and enter the managed environment -``` - -Sanity check the install with: -```bash -pixi run python -m churro.cli --help -``` - -### Configure Providers - -Copy the example environment file: -```bash -cp .example.env .env -``` -Populate only the variables you need in `.env`. -All environment variables live in `.env` and are autoloaded via `python-dotenv`. Use the table below as a quick reference to decide which credentials you must supply. +# CHURRO logo CHURRO -| Workflow | Required providers | Key variables | -|----------|-------------------|---------------| -| Azure Document Intelligence OCR (`--system azure`) or if using `docs-to-images` command without `--no-trim` | Azure Document Intelligence | `AZURE_DI_ENDPOINT`, `AZURE_DOC_KEY` | -| LLM-based OCR against Vertex AI deployments | Google Vertex AI | `VERTEX_AI_LOCATION` | -| LLM-based OCR against Azure/OpenAI deployments | Azure OpenAI or OpenAI | `AZURE_API_BASE`, `AZURE_OPENAI_API_KEY` (or `OPENAI_API_KEY`), `AZURE_API_VERSION` | -| Mistral OCR (`--system mistral_ocr`) | Mistral | `MISTRAL_API_KEY` | -| Local vLLM models (`--system finetuned` or `llm` with engines backed by `vllm/`) | Docker + Hugging Face | `LOCAL_VLLM_PORT`, `HF_TOKEN` (only if using private models) | +CHURRO is an OCR toolkit for historical document transcription, built to make handwritten and printed sources readable at high accuracy and lower cost. -When a workflow does not need a provider, leave the corresponding variables blank. See `.example.env` for full documentation of each field. -For Vertex AI usage, additionally ensure that the Google Cloud SDK is installed and authenticated: https://cloud.google.com/sdk/docs/install +It works with all major OCR proividers and vision-language models, and provides first-party support for the CHURRO 3B model and CHURRO-DS dataset. -Note that for all API LLM calls, the outputs are cached in `.litellm_cache/`, so subsequent runs with the same inputs will be much faster and free. +[![Model](https://img.shields.io/badge/Model-CHURRO%203B-8A4FFF)](https://huggingface.co/stanford-oval/churro-3B) +[![Dataset](https://img.shields.io/badge/Dataset-CHURRO--DS-0A7BBB)](https://huggingface.co/datasets/stanford-oval/churro-dataset) +[![Paper](https://img.shields.io/badge/Paper-arXiv-B31B1B)](https://arxiv.org/abs/2509.19768) +[![Docs](https://img.shields.io/badge/Docs-Documentation-8B451F)](https://stanford-oval.github.io/Churro/) +[![Leaderboard](https://img.shields.io/badge/Leaderboard-Benchmark%20Snapshot-6B7280)](https://stanford-oval.github.io/Churro/leaderboard.html) +[![GitHub Stars](https://img.shields.io/github/stars/stanford-oval/churro?style=social)](https://github.com/stanford-oval/churro/stargazers) -## CLI Workflows -The unified Typer CLI lives under `churro/cli`. All examples below assume you are inside a `pixi shell` or prefix commands with `pixi run`. -### Inference -Single image (local CHURRO model hosted via vLLM): -```bash -pixi run python -m churro.cli infer \ - --system finetuned \ - --engine churro \ - --image tests/churro_dataset_sample_1.jpeg -``` - -`finetuned` system returns HistoricalDocument XML by default; add `--strip-xml` to output plain text instead. See [HistoricalDocument XML](#historicaldocument-xml) for schema details and parsing tips. - -Optionally, add `--binarize` to pre-process each page with the bundled neural image binarizer before sending it to OCR. This can improve OCR accuracy on degraded documents. -The first run downloads the `stanford-oval/eynollah_binarizer_onnx` model. - -Batch directory with filtered suffixes and output files: -```bash -pixi run python -m churro.cli infer \ - --system finetuned \ - --engine churro \ - --image-dir path/to/images \ - --suffix png --suffix jpeg \ - --recursive \ - --output-dir workdir/texts/ \ - --skip-existing \ - --max-concurrency 8 -``` - -Use `pixi run python -m churro.cli infer --help` to see every option, including how to use other LLMs via `--system llm --engine ` arguments. - -### Preprocess PDFs and Images -If you have raw PDF scans or image directories, first use the `docs-to-images` command to convert them into page-aligned PNGs ready for OCR. -`docs-to-images` normalizes PDF scans and image directories into page-aligned PNGs. The default engine `gemini-2.5-pro-low` calls a Vertex AI model to detect double-page spreads, then calls Azure Document Intelligence to detect page boundaries and trim margins. - -Single PDF: -```bash -pixi run python -m churro.cli docs-to-images \ - --input-file path/to/file.pdf \ - --output-dir workdir/images/ -``` - -Mixed directory with custom suffix filters and dry run: -```bash -pixi run python -m churro.cli docs-to-images \ - --input-dir path/to/scans \ - --suffix pdf --suffix tif --suffix png \ - --recursive \ - --output-dir workdir/images/ \ - --dry-run -``` - -Here is how this pipeline works: -- An LLM estimates whether a rasterized page contains a two-page spread. Provide `--engine ` to swap to a different splitter if you do not have Vertex AI access. -- Margin trimming is enabled by default via Azure Document Intelligence. Use `--no-trim` to disable this stage. -- `--batch-pages`, `--queue-maxsize`, `--raster-workers`, `--page-workers`, and `--llm-concurrency-limit` balance CPU-bound rasterization and LLM throughput. -- Pages are written as `_page_XXXX.png`, even when spreads split into multiple images. - -### Benchmark on CHURRO-DS -Run end-to-end evaluation against the CHURRO dataset. The command automatically initializes any required local vLLM server before processing. -```bash -pixi run python -m churro.cli benchmark \ - --system finetuned \ - --engine churro \ - --dataset-split test \ - --input-size 0 \ - --max-concurrency 32 -``` - -Important options: -- `--system {azure,mistral_ocr,llm,finetuned}` determines which OCR backend to use. -- `--engine ` is required for `llm` and `finetuned` systems; see `churro/utils/llm/models.py` for the full `MODEL_MAP` of logical keys (GPT-4/5, Claude, Gemini, Qwen 2.5, MiniCPM, CHURRO, and more). -- `--tensor-parallel-size` / `--data-parallel-size` tune vLLM scaling for local engines. -- `--resize ` optionally resizes large images before inference. - -Optionally, add `--binarize` to pre-process each dataset page with a neural image binarizer before OCR. +- CHURRO 3B exceeds the accuracy of Gemini 2.5 Pro at 15.5x lower cost. +- CHURRO-DS contains ~100K pages from 155 historical collections spanning 22 centuries and 46 language clusters. -Outputs land under `workdir/results//_/` (the engine suffix is omitted for `azure` and `mistral_ocr`). - - -### LLM Improver -The Churro CLI supports optional post-processing with the `LLMImprover`, enabled via `--use-improver`. Pair it with `--improver-engine`. Improver can help fix OCR errors, and improve the formatting of complex documents' Markdown. - -### Backup Engines -You can supply a backup engine for LLM-based OCR systems using `--backup-engine`, and for LLM improvers using `--improver-backup-engine`. -Both backup options allow the pipeline to retry with a secondary model if the first call fails. For example, when a provider's content filter incorrectly flags historical material or when a transient outage interrupts inference. - - -### Local vLLM Container Notes -When you run `infer` or `benchmark` with an `llm` or `finetuned` system whose engine has an `hf_repo` entry, the CLI will: -- Read `LOCAL_VLLM_PORT` and `HF_TOKEN` from your environment (`churro/utils/docker/vllm.py`). -- Pull the corresponding Hugging Face repository on first launch. Expect multi-gigabyte downloads. -- Start a Docker container exposing an OpenAI-compatible API at `http://localhost:/v1`. -- Stop the container automatically when the command exits or crashes. - -Make sure the chosen port is free and that Docker is running. GPU acceleration is optional but dramatically improves throughput. - -## Adding a New OCR System -Pull requests for new VLMs and OCR backends are welcome. - -If adding a new LLM, simply add it to `utils/llm/models.py` (`MODEL_MAP`). Include an `hf_repo` for vLLM-served models. - -For entirely new OCR systems, follow all steps: -1. Register the system in `churro/systems/ocr_factory.py` so the CLI can instantiate it. -2. Implement `process_image` and `get_system_name` in a subclass of `BaseOCR`. -3. Use `--system ` with the CLI or import the factory in your own scripts. - -## HistoricalDocument XML -`HistoricalDocument` is the XML schema we use in the CHURRO dataset and model for rich transcriptions. It is specifically designed to capture complex layouts, scribal edits, and missing text, which are all common in historical documents, while preserving reading order. - -Each response contains a root `` element with optional `` details (languages, scripts, writing direction, notes) followed by one or more `` blocks. A page combines optional `
` and `