From 215651cb97160bf024448c56923ca8a8e3aa9f5a Mon Sep 17 00:00:00 2001 From: Sina Date: Sat, 4 Apr 2026 19:26:18 +0000 Subject: [PATCH 01/14] Switch to API-based implementation around core functionalities --- .example.env | 46 - .gitignore | 178 +- CONTRIBUTING.md | 45 + MANIFEST.in | 9 + README.md | 873 ++-- REPO_WORKFLOWS.md | 95 + __init__.py | 0 args.py | 145 - churro_transformers_infer.py | 192 - cli/__init__.py | 6 - cli/__main__.py | 9 - cli/benchmark.py | 105 - cli/docs_to_images.py | 180 - cli/helpers.py | 46 - cli/infer.py | 320 -- cli/main.py | 493 --- cli/text_to_historical_doc_xml.py | 319 -- config/__init__.py | 11 - config/settings.py | 155 - conftest.py | 53 +- evaluation/__init__.py | 20 - evaluation/evaluate_page.py | 206 - evaluation/historical_doc.xsd | 605 --- evaluation/metrics.py | 129 - evaluation/normalization.py | 146 - evaluation/repetition.py | 42 - evaluation/xml_utils.py | 151 - page/__init__.py | 14 - page/page.py | 246 -- page/page_object.py | 240 -- page/visualization.py | 91 - page_boundary/__init__.py | 9 - page_boundary/_constants.py | 73 - page_boundary/_image_processing.py | 123 - page_boundary/_models.py | 101 - page_boundary/_pipeline.py | 203 - page_boundary/_serialization.py | 62 - page_boundary/cli.py | 103 - page_boundary/detector.py | 65 - pixi.lock | 3596 +++++++---------- pixi.toml | 92 - pyproject.toml | 158 +- scripts/package_check.py | 308 ++ sitecustomize.py | 13 + src/churro_ocr/__init__.py | 45 + src/churro_ocr/__main__.py | 6 + src/churro_ocr/_internal/__init__.py | 1 + src/churro_ocr/_internal/image.py | 61 + src/churro_ocr/_internal/litellm.py | 356 ++ src/churro_ocr/_internal/logging.py | 64 + src/churro_ocr/_internal/pdf.py | 44 + src/churro_ocr/_internal/prompt_logging.py | 84 + src/churro_ocr/_internal/runtime.py | 24 + src/churro_ocr/cli.py | 235 ++ src/churro_ocr/document.py | 168 + src/churro_ocr/errors.py | 15 + src/churro_ocr/ocr.py | 139 + src/churro_ocr/page_detection.py | 294 ++ src/churro_ocr/prompts/__init__.py | 21 + src/churro_ocr/prompts/layout.py | 175 + src/churro_ocr/prompts/ocr.py | 54 + src/churro_ocr/providers/__init__.py | 93 + src/churro_ocr/providers/_shared.py | 93 + src/churro_ocr/providers/builder.py | 280 ++ src/churro_ocr/providers/hf.py | 459 +++ src/churro_ocr/providers/ocr.py | 262 ++ src/churro_ocr/providers/page_detection.py | 1471 +++++++ src/churro_ocr/providers/specs.py | 224 + src/churro_ocr/providers/vllm.py | 183 + src/churro_ocr/py.typed | 1 + src/churro_ocr/templates/__init__.py | 33 + src/churro_ocr/templates/base.py | 45 + src/churro_ocr/templates/hf.py | 40 + src/churro_ocr/templates/presets.py | 33 + systems/__init__.py | 52 - systems/azure_ocr.py | 33 - systems/base_ocr.py | 71 - systems/detect_layout.py | 541 --- systems/finetuned_ocr.py | 57 - systems/llm_improver.py | 142 - systems/llm_ocr.py | 150 - systems/mistral_ocr.py | 58 - systems/ocr_factory.py | 83 - tests/{ => assets}/minimal-document.pdf | Bin tests/churro_dataset_sample_1.jpeg | Bin 701122 -> 0 bytes tests/churro_dataset_sample_2.jpeg | Bin 565986 -> 0 bytes tests/conftest.py | 11 + tests/test_args_module.py | 85 - tests/test_cli.py | 222 + tests/test_cli_benchmark_unit.py | 234 -- tests/test_cli_contract.py | 245 ++ tests/test_cli_end_to_end.py | 142 - tests/test_cli_infer_unit.py | 348 -- tests/test_detect_layout_cache.py | 81 - tests/test_document_pipeline.py | 100 + tests/test_evaluation_normalization_unit.py | 38 - tests/test_hf_ocr.py | 798 ++++ tests/test_hf_ocr_integration.py | 68 + tests/test_internal_helpers.py | 388 ++ tests/test_layout_api.py | 69 + tests/test_llm_async.py | 53 - tests/test_llm_ocr.py | 16 - tests/test_logging.py | 41 + tests/test_model_map.py | 36 - tests/test_ocr_api.py | 98 + tests/test_page_detection_integration.py | 151 + tests/test_page_detection_provider_helpers.py | 427 ++ tests/test_parallel_executor.py | 72 - tests/test_pdf_pipeline.py | 163 - tests/test_pdf_pipeline_integration.py | 179 - tests/test_process_one_pdf_file.py | 93 - tests/test_provider_api_contracts.py | 145 + tests/test_providers.py | 1071 +++++ tests/test_public_api_contracts.py | 203 + tests/test_settings.py | 90 - tests/test_tooling_benchmark.py | 758 ++++ tests/test_tooling_evaluate_page.py | 99 + ...etrics_unit.py => test_tooling_metrics.py} | 90 +- tests/test_tooling_support.py | 320 ++ tests/test_vllm_config.py | 70 - tooling/__init__.py | 5 + tooling/benchmarking/__init__.py | 1 + tooling/benchmarking/benchmark.py | 476 +++ tooling/benchmarking/dataset.py | 133 + tooling/evaluation/__init__.py | 25 + tooling/evaluation/evaluate_page.py | 227 ++ tooling/evaluation/metrics.py | 129 + tooling/evaluation/normalization.py | 107 + tooling/evaluation/repetition.py | 32 + tooling/evaluation/types.py | 66 + tooling/evaluation/xml_utils.py | 53 + utils/__init__.py | 5 - utils/concurrency.py | 212 - utils/docker/__init__.py | 51 - utils/docker/container.py | 95 - utils/docker/errors.py | 16 - utils/docker/logging_utils.py | 56 - utils/docker/operations.py | 315 -- utils/docker/sdk.py | 107 - utils/docker/servers.py | 177 - utils/docker/vllm.py | 138 - utils/image/binarizer.py | 793 ---- utils/image/io.py | 22 - utils/image/transform.py | 85 - utils/llm/__init__.py | 48 - utils/llm/config.py | 104 - utils/llm/core.py | 142 - utils/llm/cost.py | 40 - utils/llm/messages.py | 141 - utils/llm/models.py | 582 --- utils/llm/shutdown.py | 94 - utils/llm/types.py | 26 - utils/llm/utils.py | 61 - utils/log_utils.py | 58 - utils/pdf/__init__.py | 21 - utils/pdf/pdfs_to_images.py | 112 - utils/pdf/runner.py | 694 ---- workdir | 1 + 158 files changed, 14287 insertions(+), 14433 deletions(-) delete mode 100644 .example.env create mode 100644 CONTRIBUTING.md create mode 100644 MANIFEST.in create mode 100644 REPO_WORKFLOWS.md delete mode 100644 __init__.py delete mode 100644 args.py delete mode 100644 churro_transformers_infer.py delete mode 100644 cli/__init__.py delete mode 100644 cli/__main__.py delete mode 100644 cli/benchmark.py delete mode 100644 cli/docs_to_images.py delete mode 100644 cli/helpers.py delete mode 100644 cli/infer.py delete mode 100644 cli/main.py delete mode 100644 cli/text_to_historical_doc_xml.py delete mode 100644 config/__init__.py delete mode 100644 config/settings.py delete mode 100644 evaluation/__init__.py delete mode 100644 evaluation/evaluate_page.py delete mode 100644 evaluation/historical_doc.xsd delete mode 100644 evaluation/metrics.py delete mode 100644 evaluation/normalization.py delete mode 100644 evaluation/repetition.py delete mode 100644 evaluation/xml_utils.py delete mode 100644 page/__init__.py delete mode 100644 page/page.py delete mode 100644 page/page_object.py delete mode 100644 page/visualization.py delete mode 100644 page_boundary/__init__.py delete mode 100644 page_boundary/_constants.py delete mode 100644 page_boundary/_image_processing.py delete mode 100644 page_boundary/_models.py delete mode 100644 page_boundary/_pipeline.py delete mode 100644 page_boundary/_serialization.py delete mode 100644 page_boundary/cli.py delete mode 100644 page_boundary/detector.py delete mode 100644 pixi.toml create mode 100644 scripts/package_check.py create mode 100644 sitecustomize.py create mode 100644 src/churro_ocr/__init__.py create mode 100644 src/churro_ocr/__main__.py create mode 100644 src/churro_ocr/_internal/__init__.py create mode 100644 src/churro_ocr/_internal/image.py create mode 100644 src/churro_ocr/_internal/litellm.py create mode 100644 src/churro_ocr/_internal/logging.py create mode 100644 src/churro_ocr/_internal/pdf.py create mode 100644 src/churro_ocr/_internal/prompt_logging.py create mode 100644 src/churro_ocr/_internal/runtime.py create mode 100644 src/churro_ocr/cli.py create mode 100644 src/churro_ocr/document.py create mode 100644 src/churro_ocr/errors.py create mode 100644 src/churro_ocr/ocr.py create mode 100644 src/churro_ocr/page_detection.py create mode 100644 src/churro_ocr/prompts/__init__.py create mode 100644 src/churro_ocr/prompts/layout.py create mode 100644 src/churro_ocr/prompts/ocr.py create mode 100644 src/churro_ocr/providers/__init__.py create mode 100644 src/churro_ocr/providers/_shared.py create mode 100644 src/churro_ocr/providers/builder.py create mode 100644 src/churro_ocr/providers/hf.py create mode 100644 src/churro_ocr/providers/ocr.py create mode 100644 src/churro_ocr/providers/page_detection.py create mode 100644 src/churro_ocr/providers/specs.py create mode 100644 src/churro_ocr/providers/vllm.py create mode 100644 src/churro_ocr/py.typed create mode 100644 src/churro_ocr/templates/__init__.py create mode 100644 src/churro_ocr/templates/base.py create mode 100644 src/churro_ocr/templates/hf.py create mode 100644 src/churro_ocr/templates/presets.py delete mode 100644 systems/__init__.py delete mode 100644 systems/azure_ocr.py delete mode 100644 systems/base_ocr.py delete mode 100644 systems/detect_layout.py delete mode 100644 systems/finetuned_ocr.py delete mode 100644 systems/llm_improver.py delete mode 100644 systems/llm_ocr.py delete mode 100644 systems/mistral_ocr.py delete mode 100644 systems/ocr_factory.py rename tests/{ => assets}/minimal-document.pdf (100%) delete mode 100644 tests/churro_dataset_sample_1.jpeg delete mode 100644 tests/churro_dataset_sample_2.jpeg create mode 100644 tests/conftest.py delete mode 100644 tests/test_args_module.py create mode 100644 tests/test_cli.py delete mode 100644 tests/test_cli_benchmark_unit.py create mode 100644 tests/test_cli_contract.py delete mode 100644 tests/test_cli_end_to_end.py delete mode 100644 tests/test_cli_infer_unit.py delete mode 100644 tests/test_detect_layout_cache.py create mode 100644 tests/test_document_pipeline.py delete mode 100644 tests/test_evaluation_normalization_unit.py create mode 100644 tests/test_hf_ocr.py create mode 100644 tests/test_hf_ocr_integration.py create mode 100644 tests/test_internal_helpers.py create mode 100644 tests/test_layout_api.py delete mode 100644 tests/test_llm_async.py delete mode 100644 tests/test_llm_ocr.py create mode 100644 tests/test_logging.py delete mode 100644 tests/test_model_map.py create mode 100644 tests/test_ocr_api.py create mode 100644 tests/test_page_detection_integration.py create mode 100644 tests/test_page_detection_provider_helpers.py delete mode 100644 tests/test_parallel_executor.py delete mode 100644 tests/test_pdf_pipeline.py delete mode 100644 tests/test_pdf_pipeline_integration.py delete mode 100644 tests/test_process_one_pdf_file.py create mode 100644 tests/test_provider_api_contracts.py create mode 100644 tests/test_providers.py create mode 100644 tests/test_public_api_contracts.py delete mode 100644 tests/test_settings.py create mode 100644 tests/test_tooling_benchmark.py create mode 100644 tests/test_tooling_evaluate_page.py rename tests/{test_evaluation_metrics_unit.py => test_tooling_metrics.py} (53%) create mode 100644 tests/test_tooling_support.py delete mode 100644 tests/test_vllm_config.py create mode 100644 tooling/__init__.py create mode 100644 tooling/benchmarking/__init__.py create mode 100644 tooling/benchmarking/benchmark.py create mode 100644 tooling/benchmarking/dataset.py create mode 100644 tooling/evaluation/__init__.py create mode 100644 tooling/evaluation/evaluate_page.py create mode 100644 tooling/evaluation/metrics.py create mode 100644 tooling/evaluation/normalization.py create mode 100644 tooling/evaluation/repetition.py create mode 100644 tooling/evaluation/types.py create mode 100644 tooling/evaluation/xml_utils.py delete mode 100644 utils/__init__.py delete mode 100644 utils/concurrency.py delete mode 100644 utils/docker/__init__.py delete mode 100644 utils/docker/container.py delete mode 100644 utils/docker/errors.py delete mode 100644 utils/docker/logging_utils.py delete mode 100644 utils/docker/operations.py delete mode 100644 utils/docker/sdk.py delete mode 100644 utils/docker/servers.py delete mode 100644 utils/docker/vllm.py delete mode 100644 utils/image/binarizer.py delete mode 100644 utils/image/io.py delete mode 100644 utils/image/transform.py delete mode 100644 utils/llm/__init__.py delete mode 100644 utils/llm/config.py delete mode 100644 utils/llm/core.py delete mode 100644 utils/llm/cost.py delete mode 100644 utils/llm/messages.py delete mode 100644 utils/llm/models.py delete mode 100644 utils/llm/shutdown.py delete mode 100644 utils/llm/types.py delete mode 100644 utils/llm/utils.py delete mode 100644 utils/log_utils.py delete mode 100644 utils/pdf/__init__.py delete mode 100644 utils/pdf/pdfs_to_images.py delete mode 100644 utils/pdf/runner.py create mode 120000 workdir 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/.gitignore b/.gitignore index 313ed49..33414bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,173 +1,23 @@ -# 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/ - -# 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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..71fe6d0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing to Churro + +Use Pixi for local development. Treat this checkout as the repo root. + +## Setup + +Install Pixi, then create the default environment from the checkout root: + +```bash +pixi install +``` + +## Common Commands + +Run these from the checkout root: + +```bash +pixi run format +pixi run lint +pixi run typecheck +pixi run test +pixi run coverage +pixi run package-check +``` + +What they do: +- `format`: run Ruff formatting on `src/` and `tests/` +- `lint`: run Ruff lint checks on `src/` and `tests/` +- `typecheck`: run `ty` on `src/` and `tests/` +- `test`: run the unit and offline test suite +- `coverage`: run the full coverage report used for release audits +- `package-check`: build and audit the wheel and sdist that would be published to PyPI + +## Benchmarking and Release Checks + +Repo-only benchmarking, evaluation outputs, and the package audit are documented in `REPO_WORKFLOWS.md`. + +## Live Integration Tests + +Live provider integration tests are skipped by default. + +- `tests/test_page_detection_integration.py` uses `CHURRO_RUN_LIVE_VERTEX_TESTS` +- `tests/test_hf_ocr_integration.py` uses `CHURRO_RUN_LIVE_HF_TESTS` + +These tests may require credentials, external services, and billable APIs. Do not enable them in routine local runs unless you intend to use those services. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..914b011 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +include LICENSE +include README.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..f4117f7 100644 --- a/README.md +++ b/README.md @@ -1,305 +1,708 @@ -

- 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: +# churro-ocr + +`churro-ocr` is a Python toolkit for OCR and page detection on historical documents. + +It gives you one consistent interface whether you are using: +- hosted multimodal models through LiteLLM +- a local OpenAI-compatible server +- local Hugging Face or vLLM models +- Azure Document Intelligence +- Mistral OCR + +If you are new to the library, the shortest path is: +1. Install the extra for the backend you want. +2. Create an `OCRBackendSpec`. +3. Build it with `build_ocr_backend(...)`. +4. Use `OCRClient` for one page or `DocumentOCRPipeline` for a full document. + +The PyPI package name is `churro-ocr`. The Python import package is `churro_ocr`. + +## Setup + +Install only the pieces you need: + ```bash -git clone https://github.com/stanford-oval/churro.git -cd churro -curl -fsSL https://pixi.sh/install.sh | bash -pixi shell -e minimal +pip install churro-ocr +pip install "churro-ocr[llm]" +pip install "churro-ocr[local]" +pip install "churro-ocr[hf]" +pip install "churro-ocr[huggingface]" +pip install "churro-ocr[vllm]" +pip install "churro-ocr[azure]" +pip install "churro-ocr[mistral]" +pip install "churro-ocr[pdf]" +pip install "churro-ocr[all]" ``` -Then run: -```bash -python churro_transformers_infer.py tests/churro_dataset_sample_1.jpeg --max-new-tokens 40 +What each extra is for: +- `llm`: hosted multimodal OCR and LLM-based page detection through LiteLLM +- `local`: OpenAI-compatible OCR servers +- `hf` and `huggingface`: equivalent extras for local Hugging Face Transformers OCR +- `vllm`: local vLLM OCR +- `azure`: Azure Document Intelligence OCR and page detection +- `mistral`: Mistral OCR +- `pdf`: PDF rasterization via pypdfium2 +- `all`: everything above + +Credential setup depends on the provider you choose: +- LiteLLM-backed models use LiteLLM's normal authentication flow. +- OpenAI-compatible servers usually need `api_base` and `api_key`. +- Azure Document Intelligence needs an endpoint and API key. +- Mistral needs an API key. + +When you need to pass connection details directly, use `LiteLLMTransportConfig` or the provider-specific options dataclasses shown below. + +If you are working from a repo checkout instead of installing from PyPI, use Pixi from the checkout root. The public workflow is `pixi run lint`, `pixi run test`, and `pixi run package-check`, not the internal Nx file. + +## Core Idea + +`churro-ocr` has a small set of concepts: + +- `DocumentPage`: one page image plus optional OCR text and metadata +- `OCRBackendSpec`: a declarative description of which OCR backend to build +- `build_ocr_backend(...)`: the factory that turns a spec into a runnable OCR backend +- `OCRClient`: OCR for one image or one `DocumentPage` +- `DocumentPageDetector`: page detection only +- `DocumentOCRPipeline`: page detection plus OCR for a complete image or PDF flow, with bounded OCR concurrency + +Most users do not need to think about prompts or preprocessing directly. `churro-ocr` ships built-in OCR model profiles and automatically applies the right prompt and output cleanup for: +- generic OCR models +- `stanford-oval/churro-3B` +- `kristaller486/dots.ocr-1.5` + +## Which API Should I Use? + +| Goal | API | +| --- | --- | +| OCR one page or one image | `OCRClient` | +| Detect page crops only | `DocumentPageDetector` | +| Run an end-to-end image/PDF OCR workflow | `DocumentOCRPipeline` | +| Try things from the shell | `churro-ocr` CLI | +| Tune backend/provider options directly | `build_ocr_backend(...)` + `OCRBackendSpec` | + +## Which OCR Backend Should I Use? + +| Provider | Install extra | Good default when | +| --- | --- | --- | +| `litellm` | `llm` | you want to use a hosted multimodal model through LiteLLM | +| `openai-compatible` | `local` | you have a local or self-hosted OpenAI-style server | +| `hf` | `hf` or `huggingface` | you want local Transformers inference in-process | +| `vllm` | `vllm` | you want higher-throughput local serving | +| `azure` | `azure` | you want Azure Document Intelligence OCR | +| `mistral` | `mistral` | you want Mistral's OCR API | + +All of these backends use the same builder: + +```python +from churro_ocr.providers import OCRBackendSpec, build_ocr_backend + +backend = build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model="vertex_ai/gemini-2.5-flash", + ) +) ``` -Expected output begins with: +## Quick Start: OCR One Image + +Use `OCRClient` when your input is already one page per image. + +```python +from churro_ocr.ocr import OCRClient +from churro_ocr.providers import OCRBackendSpec, build_ocr_backend + +backend = build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model="vertex_ai/gemini-2.5-flash", + ) +) -```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. +```python +import asyncio -### 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 +from churro_ocr.ocr import OCRClient +from churro_ocr.providers import OCRBackendSpec, build_ocr_backend + + +async def main() -> None: + backend = build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model="vertex_ai/gemini-2.5-flash", + ) + ) + page = await OCRClient(backend).aocr_image( + image_path="scan.png", + page_index=3, + source_index=7, + metadata={"job_id": "demo"}, + ) + print(page.text) + print(page.metadata) + + +asyncio.run(main()) ``` -### 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. +Async page detection for images or PDFs: -### 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 +```python +import asyncio + +from churro_ocr.page_detection import DocumentPageDetector, PageDetectionRequest + + +async def main() -> None: + detector = DocumentPageDetector() + image_result = await detector.detect_image( + PageDetectionRequest(image_path="spread.jpg", trim_margin=20) + ) + pdf_result = await detector.detect_pdf("document.pdf", dpi=300, trim_margin=20) + print(image_result.source_type, len(image_result.pages)) + print(pdf_result.source_type, len(pdf_result.pages)) + + +asyncio.run(main()) ``` -Sanity check the install with: -```bash -pixi run python -m churro.cli --help +Async end-to-end document OCR: + +```python +import asyncio + +from churro_ocr import DocumentOCRPipeline, PageDetectionRequest +from churro_ocr.providers import OCRBackendSpec, build_ocr_backend + + +async def main() -> None: + pipeline = DocumentOCRPipeline( + build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model="vertex_ai/gemini-2.5-flash", + ) + ), + max_concurrency=4, + ) + image_result = await pipeline.process_image( + PageDetectionRequest(image_path="spread.jpg", trim_margin=20), + ocr_metadata={"job_id": "demo-image"}, + ) + pdf_result = await pipeline.process_pdf( + "document.pdf", + dpi=300, + trim_margin=20, + ocr_metadata={"job_id": "demo-pdf"}, + ) + print(image_result.texts()) + print(pdf_result.as_ocr_results()[0].metadata) + + +asyncio.run(main()) +``` + +## Full Example: Detect Pages and OCR a Photographed Spread + +This example shows the complete flow for a single image that may contain multiple pages. +It uses an LLM page detector to find page crops, then OCRs each crop with the same model family. + +```python +from pathlib import Path + +from churro_ocr import DocumentOCRPipeline, PageDetectionRequest +from churro_ocr.providers import ( + LLMPageDetector, + LiteLLMTransportConfig, + OCRBackendSpec, + build_ocr_backend, +) + +INPUT_IMAGE = Path("spread.jpg") +OUTPUT_DIR = Path("output") +MODEL = "vertex_ai/gemini-2.5-flash" + +transport = LiteLLMTransportConfig() + +ocr_backend = build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model=MODEL, + transport=transport, + ) +) + +pipeline = DocumentOCRPipeline( + ocr_backend, + detection_backend=LLMPageDetector( + model=MODEL, + transport=transport, + ), + max_concurrency=4, +) + +result = pipeline.process_image_sync( + PageDetectionRequest( + image_path=INPUT_IMAGE, + trim_margin=20, + ) +) + +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + +for page in result.pages: + image_path = OUTPUT_DIR / f"page_{page.page_index:04d}.png" + text_path = OUTPUT_DIR / f"page_{page.page_index:04d}.txt" + + page.image.save(image_path) + text_path.write_text(page.text or "", encoding="utf-8") + + print( + f"page={page.page_index} " + f"provider={page.provider_name} " + f"model={page.model_name} " + f"text_file={text_path}" + ) ``` -### Configure Providers +If your input is already one page per image, skip the `detection_backend` and use `OCRClient` instead. -Copy the example environment file: -```bash -cp .example.env .env +## Backend Recipes + +### LiteLLM + +Use this for hosted multimodal models routed through LiteLLM. + +```python +from churro_ocr.providers import OCRBackendSpec, build_ocr_backend + +backend = build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model="vertex_ai/gemini-2.5-flash", + ) +) +``` + +If you need to override connection details or completion settings: + +```python +from churro_ocr.providers import LiteLLMTransportConfig, OCRBackendSpec, build_ocr_backend + +backend = build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model="gpt-4.1-mini", + transport=LiteLLMTransportConfig( + api_base="https://example.invalid/v1", + api_key="secret", + api_version="2025-01-01-preview", + completion_kwargs={"temperature": 0}, + ), + ) +) ``` -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. -| 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) | +### OpenAI-Compatible Servers + +Use this for local or self-hosted servers that expose an OpenAI-style API. + +```python +from churro_ocr.providers import ( + LiteLLMTransportConfig, + OCRBackendSpec, + build_ocr_backend, +) + +backend = build_ocr_backend( + OCRBackendSpec( + provider="openai-compatible", + model="local-model", + transport=LiteLLMTransportConfig( + api_base="http://127.0.0.1:8000/v1", + api_key="dummy", + ), + ) +) +``` -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 +### Hugging Face -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. +Use this for local Transformers inference inside the current Python process. -## 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`. +```python +from churro_ocr.providers import HuggingFaceOptions, OCRBackendSpec, build_ocr_backend -### 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 +backend = build_ocr_backend( + OCRBackendSpec( + provider="hf", + model="stanford-oval/churro-3B", + options=HuggingFaceOptions( + model_kwargs={"device_map": "auto", "torch_dtype": "auto"}, + ), + ) +) ``` -`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. +### vLLM -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. +Use this for higher-throughput local inference. -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 +```python +from churro_ocr.providers import OCRBackendSpec, VLLMOptions, build_ocr_backend + +backend = build_ocr_backend( + OCRBackendSpec( + provider="vllm", + model="stanford-oval/churro-3B", + options=VLLMOptions(), + ) +) ``` -Use `pixi run python -m churro.cli infer --help` to see every option, including how to use other LLMs via `--system llm --engine ` arguments. +### Azure Document Intelligence + +Use this when you want Azure's OCR stack instead of a general multimodal model. + +```python +from churro_ocr.providers import ( + AzureDocumentIntelligenceOptions, + OCRBackendSpec, + build_ocr_backend, +) + +backend = build_ocr_backend( + OCRBackendSpec( + provider="azure", + options=AzureDocumentIntelligenceOptions( + endpoint="https://.cognitiveservices.azure.com/", + api_key="", + ), + ) +) +``` -### 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. +### Mistral OCR -Single PDF: -```bash -pixi run python -m churro.cli docs-to-images \ - --input-file path/to/file.pdf \ - --output-dir workdir/images/ +```python +from churro_ocr.providers import MistralOptions, OCRBackendSpec, build_ocr_backend + +backend = build_ocr_backend( + OCRBackendSpec( + provider="mistral", + model="mistral-ocr-latest", + options=MistralOptions(api_key=""), + ) +) ``` -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 +## Backend Spec Reference + +`OCRBackendSpec` is the builder input shared across all OCR providers. + +| Field | Meaning | +| --- | --- | +| `provider` | One of `litellm`, `openai-compatible`, `azure`, `mistral`, `hf`, or `vllm`. | +| `model` | Required for `litellm`, `openai-compatible`, `hf`, and `vllm`. Optional for `azure`. Defaults to `mistral-ocr-latest` when omitted for `mistral`. | +| `profile` | `None`, a built-in profile name, or a custom `OCRModelProfile`. If omitted, `resolve_ocr_profile(...)` picks the built-in profile for the model id when available, otherwise the generic default profile. | +| `transport` | Shared request transport config for LiteLLM-based providers. Use this for `litellm`, `openai-compatible`, or LLM page detection. | +| `options` | Provider-specific dataclass. Use the options type that matches `provider`. | + +Provider option dataclasses: + +| Type | Used by | Required fields | Defaults and notes | +| --- | --- | --- | --- | +| `LiteLLMTransportConfig` | `litellm`, `openai-compatible`, `LLMPageDetector` | None at the dataclass level. `openai-compatible` usually needs `api_base` and `api_key`. | `completion_kwargs={}`, `cache_dir=None`, `image_detail=None`, `api_version=None`. | +| `OpenAICompatibleOptions` | `openai-compatible` | None | `model_prefix=None`. Use it when your local server expects a provider prefix before the model id. | +| `HuggingFaceOptions` | `hf` | None | `trust_remote_code=None`, `processor_kwargs={}`, `model_kwargs={}`, `generation_kwargs={}`, `vision_input_builder=None`, `backend_variant=None`. | +| `VLLMOptions` | `vllm` | None | `trust_remote_code=None`, `processor_kwargs={}`, `llm_kwargs={}`, `sampling_kwargs={}`, `limit_mm_per_prompt={}`. | +| `AzureDocumentIntelligenceOptions` | `azure` | `endpoint`, `api_key` | `model` is optional for Azure OCR in `OCRBackendSpec`. | +| `MistralOptions` | `mistral` | `api_key` | If `OCRBackendSpec.model` is omitted, the backend defaults to `mistral-ocr-latest`. | + +The library also exports `DEFAULT_OCR_MAX_TOKENS` from `churro_ocr.providers` for profile and backend integrations that need a shared OCR token budget. + +## Page Detection Only + +Use `DocumentPageDetector` when you want page crops without OCR. + +The default detector treats the whole image as one page: + +```python +from churro_ocr.page_detection import DocumentPageDetector, PageDetectionRequest + +result = DocumentPageDetector().detect_image_sync( + PageDetectionRequest(image_path="scan.png") +) + +for page in result.pages: + print(page.page_index, page.image.size) ``` -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. +For Azure-backed page detection: -### 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 +```python +from churro_ocr.page_detection import DocumentPageDetector, PageDetectionRequest +from churro_ocr.providers import AzurePageDetector + +detector = DocumentPageDetector( + backend=AzurePageDetector( + endpoint="https://.cognitiveservices.azure.com/", + api_key="", + ) +) + +result = detector.detect_image_sync( + PageDetectionRequest(image_path="scan.png", trim_margin=30) +) +``` + +For LLM-based page detection: + +```python +from churro_ocr.page_detection import DocumentPageDetector, PageDetectionRequest +from churro_ocr.providers import LLMPageDetector, LiteLLMTransportConfig + +detector = DocumentPageDetector( + backend=LLMPageDetector( + model="vertex_ai/gemini-2.5-flash", + transport=LiteLLMTransportConfig(), + ) +) + +result = detector.detect_image_sync( + PageDetectionRequest(image_path="spread.jpg", trim_margin=20) +) +``` + +## PDF OCR + +If you install the `pdf` extra, `DocumentOCRPipeline` can rasterize PDFs and OCR each page. + +```python +from churro_ocr import DocumentOCRPipeline +from churro_ocr.providers import OCRBackendSpec, build_ocr_backend + +pipeline = DocumentOCRPipeline( + build_ocr_backend( + OCRBackendSpec( + provider="litellm", + model="vertex_ai/gemini-2.5-flash", + ) + ), + max_concurrency=4, +) + +result = pipeline.process_pdf_sync("document.pdf", dpi=300, trim_margin=30) + +for page in result.pages: + print(page.page_index, page.text) ``` -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. +## Result Objects + +The main result types are stable public interfaces: + +| Type | Returned by | Important fields | +| --- | --- | --- | +| `DocumentPage` | `OCRClient`, `DocumentPageDetector`, `DocumentOCRPipeline` | `image`, `text`, `provider_name`, `model_name`, `metadata`, `ocr_metadata`, `page_index`, `source_index`, `bbox`, `polygon` | +| `OCRResult` | low-level OCR backend calls and `DocumentOCRResult.as_ocr_results()` | `text`, `provider_name`, `model_name`, `metadata` | +| `PageDetectionResult` | `DocumentPageDetector.detect_*` | `pages`, `source_type`, `metadata` | +| `DocumentOCRResult` | `DocumentOCRPipeline.process_*` | `pages`, `source_type`, `metadata`, `texts()`, `as_ocr_results()` | + +Field meanings: +- `metadata` is caller-side or detector-side metadata attached to the page or result object. +- `ocr_metadata` is provider-returned OCR metadata for one page. `DocumentOCRResult.as_ocr_results()` copies it into each `OCRResult.metadata`. +- `page_index` is the page position within the current detection or OCR result. +- `source_index` is the index in the original input source. For single-image flows it is usually `0`. For `detect_pdf(...)` and `process_pdf(...)`, it is the rasterized PDF page index the crop came from. +- `source_type` is `"image"` or `"pdf"` on `PageDetectionResult` and `DocumentOCRResult`. + +## Advanced: Custom Prompt Template for a Hugging Face Model + +Most users should rely on the built-in model profiles. If you need to override prompt rendering for a custom model, create a custom `OCRModelProfile`. + +```python +from churro_ocr import HFChatTemplate +from churro_ocr.providers import ( + HuggingFaceOptions, + OCRBackendSpec, + OCRModelProfile, + build_ocr_backend, +) + +backend = build_ocr_backend( + OCRBackendSpec( + provider="hf", + model="your-org/your-vlm", + profile=OCRModelProfile( + profile_name="custom", + template=HFChatTemplate( + system_message="Transcribe the page exactly.", + user_prompt=None, + ), + ), + options=HuggingFaceOptions(model_kwargs={"device_map": "auto"}), + ) +) +``` -Optionally, add `--binarize` to pre-process each dataset page with a neural image binarizer before OCR. +## Prompt and Template Exports -Outputs land under `workdir/results//_/` (the engine suffix is omitted for `azure` and `mistral_ocr`). +Most users should start with the built-in profiles and templates rather than building prompts from scratch. +Built-in OCR profiles resolved by `resolve_ocr_profile(...)`: +- the generic default profile for unknown models +- `stanford-oval/churro-3B`, which uses `CHURRO_3B_XML_TEMPLATE` +- `kristaller486/dots.ocr-1.5`, which uses `DOTS_OCR_1_5_OCR_TEMPLATE` -### 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. +Useful public template exports: -### 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. +| Export | Module | Use case | +| --- | --- | --- | +| `HFChatTemplate` | `churro_ocr.templates` | Build a Hugging Face chat-style multimodal prompt with optional system text, user prompt text, and image inclusion. | +| `DEFAULT_OCR_TEMPLATE` | `churro_ocr.templates` | Generic OCR prompt template used by the default model profile. | +| `CHURRO_3B_XML_TEMPLATE` | `churro_ocr.templates` | Built-in template for `stanford-oval/churro-3B`. | +| `DOTS_OCR_1_5_OCR_TEMPLATE` | `churro_ocr.templates` | Built-in template for `kristaller486/dots.ocr-1.5`. | +| `OCRPromptTemplate` | `churro_ocr.templates` | Base template protocol/type for custom profile integration. | +Useful public prompt exports: -### 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. +| Export | Module | Use case | +| --- | --- | --- | +| `DEFAULT_OCR_SYSTEM_PROMPT` | `churro_ocr.prompts` | Default system instruction for generic OCR prompting. | +| `DEFAULT_OCR_USER_PROMPT` | `churro_ocr.prompts` | Default user prompt for plain OCR output. | +| `DEFAULT_MARKDOWN_OCR_USER_PROMPT` | `churro_ocr.prompts` | Default user prompt when markdown-style OCR output is preferred. | +| `DEFAULT_OCR_OUTPUT_TAG` | `churro_ocr.prompts` | Shared tag name used by the default OCR postprocessor. | +| `DEFAULT_BOUNDARY_DETECTION_PROMPT` | `churro_ocr.prompts` | Default prompt used by LLM-based page and text-block boundary detection helpers. | +| `strip_ocr_output_tag(...)` | `churro_ocr.prompts` | Remove the default OCR wrapper tag from model output before downstream evaluation or postprocessing. | -Make sure the chosen port is free and that Docker is running. GPU acceleration is optional but dramatically improves throughput. +If you need prompt overrides without replacing the entire backend, pass a custom `OCRModelProfile` through `OCRBackendSpec(profile=...)` and keep the provider-specific options unchanged. -## Adding a New OCR System -Pull requests for new VLMs and OCR backends are welcome. +## CLI -If adding a new LLM, simply add it to `utils/llm/models.py` (`MODEL_MAP`). Include an `hf_repo` for vLLM-served models. +Use the CLI when you want a quick sanity check before writing Python code. -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. +Use `churro-ocr --help` or `python -m churro_ocr --help` to see the top-level commands. -## 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. +`churro-ocr extract-pages` writes one PNG file per detected page into `--output-dir`. +Files are named sequentially like `page_0000.png`, `page_0001.png`, and so on. +The command also prints each written file path to stdout. -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 `