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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Examples:

### Python Style
- **Line length**: 100 characters (Ruff configured)
- **Python version**: 3.11+ (uses modern type hints like `list[str]`, `tuple[float, float]`)
- **Python version**: 3.10+
- **Type checking**: Strict mode with pyright
- **Linting**: Ruff with rules E, F, I, N, W enabled
- Type annotations required on all public functions
Expand Down
62 changes: 62 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# OCR Bridge - EasyOCR Engine

## Project Overview
`ocrbridge-easyocr` is a Python-based plugin for the [OCR Bridge](https://github.com/OCRBridge/ocr-service) architecture. It integrates the [EasyOCR](https://github.com/JaidedAI/EasyOCR) library to provide deep learning-based Optical Character Recognition (OCR) capabilities.

**Key Features:**
* **Plugin Architecture:** Implements the `OCREngine` interface from `ocrbridge-core`.
* **Deep Learning:** Uses EasyOCR (powered by PyTorch) for high-accuracy text recognition.
* **Multilingual:** Supports 80+ languages, with a focus on Asian scripts.
* **GPU Acceleration:** Automatically detects and utilizes CUDA GPUs via PyTorch.
* **Format Support:** Handles images (JPG, PNG, TIFF) and PDFs (via `pdf2image`).
* **Standard Output:** Produces HOCR (HTML-based XML) output with bounding boxes.

## Development Setup

### Prerequisites
* **Python:** 3.10+
* **Package Manager:** `uv` (Unified Python packaging)
* **System Libraries:**
* `poppler-utils` (Required by `pdf2image` for PDF processing)
* CUDA-compatible GPU drivers (Optional, for GPU acceleration)

### Building and Running
This project uses a `Makefile` to orchestrate common development tasks, wrapping `uv` commands.

| Command | Description |
| :--- | :--- |
| `make install` | Install dependencies, including dev extras. |
| `make test` | Run the test suite using `pytest`. |
| `make lint` | Run `ruff` for code linting. |
| `make format` | Format code using `ruff`. |
| `make typecheck` | Run static type checking with `pyright`. |
| `make check` | Run all quality checks: `lint`, `typecheck`, and `test`. |
| `make all` | Run `check` and `format`. |
| `uv build` | Build the distribution packages (wheel/sdist). |

### Architecture
* **Entry Point:** The engine is registered via `project.entry-points` in `pyproject.toml` as `ocrbridge.engines.easyocr:EasyOCREngine`.
* **Core Logic:** `src/ocrbridge/engines/easyocr/engine.py` contains the `EasyOCREngine` class.
* `_create_reader`: Initializes the EasyOCR reader (lazy-loaded).
* `process`: Main method to handle files; routes to `_process_image` or `_process_pdf`.
* `_to_hocr`: Converts EasyOCR's specific output format to standard HOCR XML.
* **Configuration:** `src/ocrbridge/engines/easyocr/models.py` defines `EasyOCRParams` (languages, thresholds) using Pydantic.

## Development Conventions

### Code Style
* **Formatting:** Enforced by `ruff`. Line length is 100 characters.
* **Typing:** Strict type checking with `pyright`. All public functions must have type annotations.
* **Imports:** Sorted and organized by `ruff`.

### Testing
* **Framework:** `pytest`.
* **Location:** Tests are located in the `tests/` directory.
* **Markers:**
* `@pytest.mark.integration`: Tests requiring external binaries (e.g., Tesseract, though this is EasyOCR repo, the marker exists in config).
* `@pytest.mark.slow`: Long-running tests.
* **Samples:** `samples/` directory contains test assets (images, PDFs).

### Versioning & Release
* **Semantic Versioning:** Managed by `python-semantic-release`.
* **Commits:** Must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification (e.g., `feat:`, `fix:`, `docs:`). This is enforced by `commitlint` (via Node.js hooks) and checked in CI.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ format: install
$(UV) run $(RUFF) format src tests

typecheck: install
$(UV) run $(PYRIGHT)
$(UV) run $(PYRIGHT) --project pyproject.toml

test: install
$(UV) run pytest
Expand Down
12 changes: 8 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ name = "ocrbridge-easyocr"
version = "1.0.0"
description = "EasyOCR engine for OCR Bridge"
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.10"
license = {text = "MIT"}

dependencies = [
"ocrbridge-core>=0.1.0",
"ocrbridge-core>=2.0.0",
"easyocr>=1.7.2",
"torch>=2.1.0",
"pdf2image>=1.17.0",
Expand All @@ -27,7 +27,7 @@ dev = [
]
test = [
"pytest~=8.0",
"pytest-mock>=3.15.1",
"pytest-mock>=3.12.0",
]

# Entry point for engine discovery
Expand All @@ -43,7 +43,7 @@ packages = ["src/ocrbridge"]

[tool.ruff]
line-length = 100
target-version = "py311"
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
Expand All @@ -64,6 +64,10 @@ markers = [
"integration: marks tests as integration tests requiring Tesseract binary",
"slow: marks tests as slow running",
]
filterwarnings = [
"ignore:The argument 'device' of Tensor.pin_memory.*:DeprecationWarning",
"ignore:The argument 'device' of Tensor.is_pinned.*:DeprecationWarning",
]

[tool.coverage.run]
source = ["src"]
Expand Down
40 changes: 20 additions & 20 deletions src/ocrbridge/engines/easyocr/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""EasyOCR engine parameter models."""

from pydantic import Field, field_validator
from pydantic import Field, ValidationInfo, field_validator

from ocrbridge.core.models import OCREngineParams
from ocrbridge.core.validation import (
validate_list_length,
validate_probability,
validate_whitelist,
)

# EasyOCR supported languages (80+ languages)
EASYOCR_SUPPORTED_LANGUAGES = {
Expand Down Expand Up @@ -119,31 +124,26 @@ class EasyOCRParams(OCREngineParams):
@classmethod
def validate_languages(cls, v: list[str]) -> list[str]:
"""Validate EasyOCR language codes against supported languages."""
if not v:
raise ValueError("At least one language required for EasyOCR")
# Use core utilities for common validations
validate_list_length(v, min_length=1, max_length=5, field_name="languages")

if len(v) > 5:
raise ValueError("Maximum 5 languages allowed for EasyOCR")

# Check all languages are supported
invalid_langs = [lang for lang in v if lang not in EASYOCR_SUPPORTED_LANGUAGES]

if invalid_langs:
# Engine-specific: Check against EasyOCR whitelist with improved error
try:
validate_whitelist(v, EASYOCR_SUPPORTED_LANGUAGES, field_name="EasyOCR languages")
except ValueError as e:
# Add helpful hint about format difference
raise ValueError(
(
f"Unsupported EasyOCR language codes: {invalid_langs}. "
"Use EasyOCR format (e.g., 'en', 'ch_sim', 'ja'), "
"not Tesseract format ('eng', 'chi_sim')"
)
f"{e}. Use EasyOCR format (e.g., 'en', 'ch_sim', 'ja'), "
"not Tesseract format ('eng', 'chi_sim')"
)

return v

@field_validator("text_threshold", "link_threshold")
@classmethod
def validate_threshold(cls, v: float) -> float:
def validate_threshold(cls, v: float, info: ValidationInfo) -> float:
"""Validate threshold is within valid range."""
if not 0.0 <= v <= 1.0:
raise ValueError("Threshold must be between 0.0 and 1.0")

return v
# Use core utility for probability validation
# Check if field_name is available (it should be for field validators)
field_name = info.field_name or "threshold"
return validate_probability(v, field_name=field_name)
1 change: 0 additions & 1 deletion tests/__init__.py

This file was deleted.

4 changes: 2 additions & 2 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_invalid_language_code(self) -> None:
EasyOCRParams(languages=["eng"]) # Tesseract format, not EasyOCR

error = exc_info.value.errors()[0]
assert "Unsupported EasyOCR language codes" in error["msg"]
assert "Invalid values for 'EasyOCR languages'" in error["msg"]
assert "eng" in error["msg"]

def test_invalid_multiple_language_codes(self) -> None:
Expand All @@ -51,7 +51,7 @@ def test_invalid_multiple_language_codes(self) -> None:
EasyOCRParams(languages=["eng", "chi_sim", "invalid"])

error = exc_info.value.errors()[0]
assert "Unsupported EasyOCR language codes" in error["msg"]
assert "Invalid values for 'EasyOCR languages'" in error["msg"]

def test_empty_languages_list(self) -> None:
"""Test EasyOCRParams rejects empty language list."""
Expand Down