Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog

All notable changes to `churro-ocr` will be documented in this file.

## 0.3.0

### Added

- Added UV-managed runtime installers through `churro-ocr install` for `llm`, `local`, `hf`, `azure`, `mistral`, `pdf`, and `all`.
- Added built-in OCR profiles and provider integrations for Chandra OCR 2, Dots OCR 1.5, dots.mocr, DeepSeek OCR 2, FireRed OCR, GLM-OCR, Infinity-Parser 7B, Liquid LFM2.5-VL 1.6B, MinerU2.5, Nanonets OCR2, olmOCR 2 7B, PaddleOCR-VL 1.5, and Qianfan OCR.
- Added richer prompt, template, and response-processing helpers for OCR backends that emit markdown or HTML.
- Added benchmark leaderboard improvements, including expandable per-language score views and refreshed benchmark coverage across newly supported models.

### Changed

- Updated the `hf` extra to `transformers>=5,<6` and moved local PyTorch installation behind the runtime installer workflow.
- Reworked the docs around a CLI-first onboarding path with expanded provider guidance, advanced customization docs, and a more detailed PyPI/README presentation.
- Tightened typing and internal provider boundaries across OCR, page detection, evaluation, and helper modules.

### Fixed

- Improved retry handling with a total timeout budget, better transient provider-error handling, and more graceful handling for timed-out OCR pages.
- Hardened LiteLLM request cleanup, unmapped-model handling, and default OCR parsing for providers that return empty or markdown-heavy responses.
- Fixed evaluation and benchmarking stability issues around multiprocessing pools, cached LiteLLM clients, and OCR metadata retention.

### Breaking Changes

- Removed the `vllm` extra and the in-process `churro_ocr.providers.vllm` backend. Serve vLLM separately and use the `openai-compatible` backend instead.
- Stopped re-exporting template classes from the top-level `churro_ocr` package. Import `HFChatTemplate`, `OCRPromptTemplate`, and related helpers from `churro_ocr.templates`.
6 changes: 3 additions & 3 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"

[project]
name = "churro-ocr"
version = "0.2.0"
description = "OCR for historical documents"
version = "0.3.0"
description = "OCR and page detection for historical documents"
readme = { file = "docs/pypi.md", content-type = "text/markdown" }
requires-python = ">=3.12"
license = "Apache-2.0"
Expand All @@ -20,7 +20,7 @@ dependencies = [
"loguru>=0.7.2,<1",
"Pillow>=10.4.0,<12",
"rich>=13.9.2,<14",
"tenacity>=9.1.2,<10",
"tenacity>=9.0.0,<10",
"typer>=0.12.3,<1",
]

Expand Down
43 changes: 19 additions & 24 deletions src/churro_ocr/providers/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,11 @@

from __future__ import annotations

from importlib import import_module
from typing import TYPE_CHECKING

from churro_ocr._internal.litellm import LiteLLMTransport
from churro_ocr.errors import ConfigurationError
from churro_ocr.providers.hf import (
ChandraOCR2OCRBackend,
DeepSeekOCR2OCRBackend,
DotsMOCROCRBackend,
DotsOCR15OCRBackend,
GlmOCROCRBackend,
HuggingFaceVisionOCRBackend,
LFM25VLOCRBackend,
MinerU25OCRBackend,
PaddleOCRVL15OCRBackend,
QianfanOCROCRBackend,
_default_dots_ocr_1_5_model_kwargs,
)
from churro_ocr.providers.ocr import (
AzureDocumentIntelligenceOCRBackend,
LiteLLMVisionOCRBackend,
Expand All @@ -40,6 +28,8 @@
from churro_ocr.templates import MINERU2_5_2509_1_2B_MODEL_ID

if TYPE_CHECKING:
from types import ModuleType

from churro_ocr.ocr import OCRBackend


Expand Down Expand Up @@ -137,6 +127,10 @@ def _resolve_model_name(profile: OCRModelProfile, model: str | None, *, fallback
return fallback


def _load_huggingface_backends() -> ModuleType:
return import_module("churro_ocr.providers.hf")


def _build_litellm_backend(spec: OCRBackendSpec, profile: OCRModelProfile) -> OCRBackend:
if spec.model is None:
message = "OCR provider 'litellm' requires `model`."
Expand Down Expand Up @@ -191,27 +185,28 @@ def _build_huggingface_backend(spec: OCRBackendSpec, profile: OCRModelProfile) -
profile.huggingface,
_ensure_options_type(spec.options, HuggingFaceOptions, provider=spec.provider),
)
backend_cls: type[HuggingFaceVisionOCRBackend] = HuggingFaceVisionOCRBackend
hf_backends = _load_huggingface_backends()
backend_cls = hf_backends.HuggingFaceVisionOCRBackend
model_kwargs = dict(options.model_kwargs)
if options.backend_variant in {"dots-ocr-1.5", "dots-mocr"}:
backend_cls = DotsOCR15OCRBackend
backend_cls = hf_backends.DotsOCR15OCRBackend
if options.backend_variant == "dots-mocr":
backend_cls = DotsMOCROCRBackend
model_kwargs = _merge_mapping(_default_dots_ocr_1_5_model_kwargs(), model_kwargs)
backend_cls = hf_backends.DotsMOCROCRBackend
model_kwargs = _merge_mapping(hf_backends._default_dots_ocr_1_5_model_kwargs(), model_kwargs)
elif options.backend_variant == "glm-ocr":
backend_cls = GlmOCROCRBackend
backend_cls = hf_backends.GlmOCROCRBackend
elif options.backend_variant == "deepseek-ocr-2":
backend_cls = DeepSeekOCR2OCRBackend
backend_cls = hf_backends.DeepSeekOCR2OCRBackend
elif options.backend_variant == "chandra-ocr-2":
backend_cls = ChandraOCR2OCRBackend
backend_cls = hf_backends.ChandraOCR2OCRBackend
elif options.backend_variant == "mineru2.5":
backend_cls = MinerU25OCRBackend
backend_cls = hf_backends.MinerU25OCRBackend
elif options.backend_variant == "paddleocr-vl-1.5":
backend_cls = PaddleOCRVL15OCRBackend
backend_cls = hf_backends.PaddleOCRVL15OCRBackend
elif options.backend_variant == "lfm2.5-vl":
backend_cls = LFM25VLOCRBackend
backend_cls = hf_backends.LFM25VLOCRBackend
elif options.backend_variant == "qianfan-ocr":
backend_cls = QianfanOCROCRBackend
backend_cls = hf_backends.QianfanOCROCRBackend
return backend_cls(
model_id=spec.model,
template=profile.template,
Expand Down
22 changes: 22 additions & 0 deletions tests/test_package_check.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import importlib
import sys
from email.message import Message
from importlib import metadata as importlib_metadata
from importlib.util import module_from_spec, spec_from_file_location
Expand Down Expand Up @@ -55,3 +57,23 @@ def test_local_runtime_packaging_policy_rejects_direct_torch_runtime_pin() -> No

with pytest.raises(RuntimeError, match="must not pin local PyTorch"):
package_check._assert_local_runtime_packaging_policy(metadata_message)


def test_cli_import_does_not_eagerly_import_hf_backend_module() -> None:
module_names = (
"churro_ocr.cli",
"churro_ocr.providers.builder",
"churro_ocr.providers.hf",
)
saved_modules = {name: sys.modules.pop(name, None) for name in module_names}

try:
cli_module = importlib.import_module("churro_ocr.cli")
assert cli_module.app is not None
assert "churro_ocr.providers.hf" not in sys.modules
finally:
for name in module_names:
sys.modules.pop(name, None)
for name, module in saved_modules.items():
if module is not None:
sys.modules[name] = module