natural-pdf 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/finetuning/index.md +176 -0
- docs/ocr/index.md +34 -47
- docs/tutorials/01-loading-and-extraction.ipynb +34 -1536
- docs/tutorials/02-finding-elements.ipynb +42 -42
- docs/tutorials/03-extracting-blocks.ipynb +17 -17
- docs/tutorials/04-table-extraction.ipynb +12 -12
- docs/tutorials/05-excluding-content.ipynb +30 -30
- docs/tutorials/06-document-qa.ipynb +28 -28
- docs/tutorials/07-layout-analysis.ipynb +63 -35
- docs/tutorials/07-working-with-regions.ipynb +55 -51
- docs/tutorials/07-working-with-regions.md +2 -2
- docs/tutorials/08-spatial-navigation.ipynb +60 -60
- docs/tutorials/09-section-extraction.ipynb +113 -113
- docs/tutorials/10-form-field-extraction.ipynb +78 -50
- docs/tutorials/11-enhanced-table-processing.ipynb +6 -6
- docs/tutorials/12-ocr-integration.ipynb +149 -131
- docs/tutorials/12-ocr-integration.md +0 -13
- docs/tutorials/13-semantic-search.ipynb +313 -873
- natural_pdf/__init__.py +21 -22
- natural_pdf/analyzers/layout/gemini.py +280 -0
- natural_pdf/analyzers/layout/layout_manager.py +28 -1
- natural_pdf/analyzers/layout/layout_options.py +11 -0
- natural_pdf/analyzers/layout/yolo.py +6 -2
- natural_pdf/collections/pdf_collection.py +24 -0
- natural_pdf/core/element_manager.py +18 -13
- natural_pdf/core/page.py +174 -36
- natural_pdf/core/pdf.py +156 -42
- natural_pdf/elements/base.py +9 -17
- natural_pdf/elements/collections.py +99 -38
- natural_pdf/elements/region.py +77 -37
- natural_pdf/elements/text.py +5 -0
- natural_pdf/exporters/__init__.py +4 -0
- natural_pdf/exporters/base.py +61 -0
- natural_pdf/exporters/paddleocr.py +345 -0
- natural_pdf/ocr/__init__.py +57 -36
- natural_pdf/ocr/engine.py +160 -49
- natural_pdf/ocr/engine_easyocr.py +178 -157
- natural_pdf/ocr/engine_paddle.py +114 -189
- natural_pdf/ocr/engine_surya.py +87 -144
- natural_pdf/ocr/ocr_factory.py +125 -0
- natural_pdf/ocr/ocr_manager.py +65 -89
- natural_pdf/ocr/ocr_options.py +8 -13
- natural_pdf/ocr/utils.py +113 -0
- natural_pdf/templates/finetune/fine_tune_paddleocr.md +415 -0
- natural_pdf/templates/spa/css/style.css +334 -0
- natural_pdf/templates/spa/index.html +31 -0
- natural_pdf/templates/spa/js/app.js +472 -0
- natural_pdf/templates/spa/words.txt +235976 -0
- natural_pdf/utils/debug.py +34 -0
- natural_pdf/utils/identifiers.py +33 -0
- natural_pdf/utils/packaging.py +485 -0
- natural_pdf/utils/text_extraction.py +44 -64
- natural_pdf/utils/visualization.py +1 -1
- {natural_pdf-0.1.5.dist-info → natural_pdf-0.1.7.dist-info}/METADATA +44 -20
- {natural_pdf-0.1.5.dist-info → natural_pdf-0.1.7.dist-info}/RECORD +58 -47
- {natural_pdf-0.1.5.dist-info → natural_pdf-0.1.7.dist-info}/WHEEL +1 -1
- {natural_pdf-0.1.5.dist-info → natural_pdf-0.1.7.dist-info}/top_level.txt +0 -1
- natural_pdf/templates/ocr_debug.html +0 -517
- tests/test_loading.py +0 -50
- tests/test_optional_deps.py +0 -298
- {natural_pdf-0.1.5.dist-info → natural_pdf-0.1.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
import logging
|
2
|
+
import importlib.util
|
3
|
+
from typing import Dict, Any, Optional, Type, Union, List
|
4
|
+
|
5
|
+
from .engine import OCREngine
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
|
10
|
+
class OCRFactory:
|
11
|
+
"""Factory for creating and managing OCR engines with optional dependencies."""
|
12
|
+
|
13
|
+
@staticmethod
|
14
|
+
def create_engine(engine_type: str, **kwargs) -> OCREngine:
|
15
|
+
"""Create and return an OCR engine instance.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
engine_type: One of 'surya', 'easyocr', 'paddle'
|
19
|
+
**kwargs: Arguments to pass to the engine constructor
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
An initialized OCR engine
|
23
|
+
|
24
|
+
Raises:
|
25
|
+
ImportError: If the required dependencies aren't installed
|
26
|
+
ValueError: If the engine_type is unknown
|
27
|
+
"""
|
28
|
+
if engine_type == "surya":
|
29
|
+
try:
|
30
|
+
from .engine_surya import SuryaOCREngine
|
31
|
+
|
32
|
+
return SuryaOCREngine(**kwargs)
|
33
|
+
except ImportError:
|
34
|
+
raise ImportError(
|
35
|
+
"Surya engine requires the 'surya' package. " "Install with: pip install surya"
|
36
|
+
)
|
37
|
+
elif engine_type == "easyocr":
|
38
|
+
try:
|
39
|
+
from .engine_easyocr import EasyOCREngine
|
40
|
+
|
41
|
+
return EasyOCREngine(**kwargs)
|
42
|
+
except ImportError:
|
43
|
+
raise ImportError(
|
44
|
+
"EasyOCR engine requires the 'easyocr' package. "
|
45
|
+
"Install with: pip install easyocr"
|
46
|
+
)
|
47
|
+
elif engine_type == "paddle":
|
48
|
+
try:
|
49
|
+
from .engine_paddle import PaddleOCREngine
|
50
|
+
|
51
|
+
return PaddleOCREngine(**kwargs)
|
52
|
+
except ImportError:
|
53
|
+
raise ImportError(
|
54
|
+
"PaddleOCR engine requires 'paddleocr' and 'paddlepaddle'. "
|
55
|
+
"Install with: pip install paddleocr paddlepaddle"
|
56
|
+
)
|
57
|
+
else:
|
58
|
+
raise ValueError(f"Unknown engine type: {engine_type}")
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
def list_available_engines() -> Dict[str, bool]:
|
62
|
+
"""Returns a dictionary of engine names and their availability status."""
|
63
|
+
engines = {}
|
64
|
+
|
65
|
+
# Check Surya
|
66
|
+
try:
|
67
|
+
engines["surya"] = importlib.util.find_spec("surya") is not None
|
68
|
+
except ImportError:
|
69
|
+
engines["surya"] = False
|
70
|
+
|
71
|
+
# Check EasyOCR
|
72
|
+
try:
|
73
|
+
engines["easyocr"] = importlib.util.find_spec("easyocr") is not None
|
74
|
+
except ImportError:
|
75
|
+
engines["easyocr"] = False
|
76
|
+
|
77
|
+
# Check PaddleOCR
|
78
|
+
try:
|
79
|
+
paddle = (
|
80
|
+
importlib.util.find_spec("paddle") is not None
|
81
|
+
or importlib.util.find_spec("paddlepaddle") is not None
|
82
|
+
)
|
83
|
+
paddleocr = importlib.util.find_spec("paddleocr") is not None
|
84
|
+
engines["paddle"] = paddle and paddleocr
|
85
|
+
except ImportError:
|
86
|
+
engines["paddle"] = False
|
87
|
+
|
88
|
+
return engines
|
89
|
+
|
90
|
+
@staticmethod
|
91
|
+
def get_recommended_engine(**kwargs) -> OCREngine:
|
92
|
+
"""Returns the best available OCR engine based on what's installed.
|
93
|
+
|
94
|
+
First tries engines in order of preference: EasyOCR, Paddle, Surya.
|
95
|
+
If none are available, raises ImportError with installation instructions.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
**kwargs: Arguments to pass to the engine constructor
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
The best available OCR engine instance
|
102
|
+
|
103
|
+
Raises:
|
104
|
+
ImportError: If no engines are available
|
105
|
+
"""
|
106
|
+
available = OCRFactory.list_available_engines()
|
107
|
+
|
108
|
+
# Try engines in order of recommendation
|
109
|
+
if available.get("easyocr", False):
|
110
|
+
logger.info("Using EasyOCR engine (recommended)")
|
111
|
+
return OCRFactory.create_engine("easyocr", **kwargs)
|
112
|
+
elif available.get("paddle", False):
|
113
|
+
logger.info("Using PaddleOCR engine")
|
114
|
+
return OCRFactory.create_engine("paddle", **kwargs)
|
115
|
+
elif available.get("surya", False):
|
116
|
+
logger.info("Using Surya OCR engine")
|
117
|
+
return OCRFactory.create_engine("surya", **kwargs)
|
118
|
+
|
119
|
+
# If we get here, no engines are available
|
120
|
+
raise ImportError(
|
121
|
+
"No OCR engines available. Please install at least one of: \n"
|
122
|
+
"- EasyOCR (recommended): pip install easyocr\n"
|
123
|
+
"- PaddleOCR: pip install paddleocr paddlepaddle\n"
|
124
|
+
"- Surya OCR: pip install surya"
|
125
|
+
)
|
natural_pdf/ocr/ocr_manager.py
CHANGED
@@ -9,8 +9,8 @@ from PIL import Image
|
|
9
9
|
from .engine import OCREngine
|
10
10
|
from .engine_easyocr import EasyOCREngine
|
11
11
|
from .engine_paddle import PaddleOCREngine
|
12
|
-
from .engine_surya import SuryaOCREngine
|
13
|
-
from .ocr_options import OCROptions
|
12
|
+
from .engine_surya import SuryaOCREngine
|
13
|
+
from .ocr_options import OCROptions
|
14
14
|
from .ocr_options import BaseOCROptions, EasyOCROptions, PaddleOCROptions, SuryaOCROptions
|
15
15
|
|
16
16
|
logger = logging.getLogger(__name__)
|
@@ -27,15 +27,6 @@ class OCRManager:
|
|
27
27
|
# Add other engines here
|
28
28
|
}
|
29
29
|
|
30
|
-
# Define the limited set of kwargs allowed for the simple apply_ocr call
|
31
|
-
SIMPLE_MODE_ALLOWED_KWARGS = {
|
32
|
-
"engine",
|
33
|
-
"languages",
|
34
|
-
"min_confidence",
|
35
|
-
"device",
|
36
|
-
# Add image pre-processing args like 'resolution', 'width' if handled here
|
37
|
-
}
|
38
|
-
|
39
30
|
def __init__(self):
|
40
31
|
"""Initializes the OCR Manager."""
|
41
32
|
self._engine_instances: Dict[str, OCREngine] = {} # Cache for engine instances
|
@@ -49,16 +40,16 @@ class OCRManager:
|
|
49
40
|
f"Unknown OCR engine: '{engine_name}'. Available: {list(self.ENGINE_REGISTRY.keys())}"
|
50
41
|
)
|
51
42
|
|
52
|
-
# Surya engine might manage its own predictor state, consider if caching instance is always right
|
53
|
-
# For now, we cache the engine instance itself.
|
54
43
|
if engine_name not in self._engine_instances:
|
55
44
|
logger.info(f"Creating instance of engine: {engine_name}")
|
56
45
|
engine_class = self.ENGINE_REGISTRY[engine_name]["class"]
|
57
46
|
engine_instance = engine_class() # Instantiate first
|
58
47
|
if not engine_instance.is_available():
|
59
48
|
# Check availability before storing
|
49
|
+
# Construct helpful error message with install hint
|
50
|
+
install_hint = f"pip install 'natural-pdf[{engine_name}]'"
|
60
51
|
raise RuntimeError(
|
61
|
-
f"Engine '{engine_name}' is not available. Please
|
52
|
+
f"Engine '{engine_name}' is not available. Please install the required dependencies: {install_hint}"
|
62
53
|
)
|
63
54
|
self._engine_instances[engine_name] = engine_instance # Store if available
|
64
55
|
|
@@ -66,106 +57,91 @@ class OCRManager:
|
|
66
57
|
|
67
58
|
def apply_ocr(
|
68
59
|
self,
|
69
|
-
images: Union[Image.Image, List[Image.Image]],
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
60
|
+
images: Union[Image.Image, List[Image.Image]],
|
61
|
+
# --- Explicit Common Parameters ---
|
62
|
+
engine: Optional[str] = None,
|
63
|
+
languages: Optional[List[str]] = None,
|
64
|
+
min_confidence: Optional[float] = None,
|
65
|
+
device: Optional[str] = None,
|
66
|
+
detect_only: bool = False,
|
67
|
+
# --- Engine-Specific Options ---
|
68
|
+
options: Optional[Any] = None, # e.g. EasyOCROptions(), PaddleOCROptions()
|
69
|
+
) -> Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]]:
|
74
70
|
"""
|
75
|
-
Applies OCR to a single image or a batch of images
|
76
|
-
keyword arguments or an options object.
|
71
|
+
Applies OCR to a single image or a batch of images.
|
77
72
|
|
78
73
|
Args:
|
79
74
|
images: A single PIL Image or a list of PIL Images to process.
|
80
|
-
engine: Name of the engine
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
75
|
+
engine: Name of the engine (e.g., 'easyocr', 'paddle', 'surya').
|
76
|
+
Defaults to 'easyocr' if not specified.
|
77
|
+
languages: List of language codes (e.g., ['en', 'fr'], ['en', 'german']).
|
78
|
+
**Passed directly to the engine.** Must be codes understood
|
79
|
+
by the specific engine. No mapping is performed by the manager.
|
80
|
+
min_confidence: Minimum confidence threshold (0.0-1.0).
|
81
|
+
Passed directly to the engine.
|
82
|
+
device: Device string (e.g., 'cpu', 'cuda').
|
83
|
+
Passed directly to the engine.
|
84
|
+
detect_only: If True, only detect text regions, do not perform OCR.
|
85
|
+
options: An engine-specific options object (e.g., EasyOCROptions) or dict
|
86
|
+
containing additional parameters specific to the chosen engine.
|
87
|
+
Passed directly to the engine.
|
87
88
|
|
88
89
|
Returns:
|
89
90
|
If input is a single image: List of result dictionaries.
|
90
|
-
If input is a list of images: List of lists of result dictionaries
|
91
|
-
corresponding to each input image.
|
91
|
+
If input is a list of images: List of lists of result dictionaries.
|
92
92
|
|
93
93
|
Raises:
|
94
94
|
ValueError: If the engine name is invalid.
|
95
|
-
TypeError: If
|
96
|
-
|
97
|
-
RuntimeError: If the selected engine is not available.
|
95
|
+
TypeError: If input 'images' is not valid or options type is incompatible.
|
96
|
+
RuntimeError: If the selected engine is not available or processing fails.
|
98
97
|
"""
|
99
|
-
final_options: BaseOCROptions
|
100
|
-
selected_engine_name: str
|
101
|
-
|
102
98
|
# --- Validate input type ---
|
103
99
|
is_batch = isinstance(images, list)
|
104
100
|
if not is_batch and not isinstance(images, Image.Image):
|
105
101
|
raise TypeError("Input 'images' must be a PIL Image or a list of PIL Images.")
|
106
|
-
# Allow engines to handle non-PIL images in list if they support it/log warnings
|
107
|
-
# if is_batch and not all(isinstance(img, Image.Image) for img in images):
|
108
|
-
# logger.warning("Batch may contain items that are not PIL Images.")
|
109
|
-
|
110
|
-
# --- Determine Options and Engine ---
|
111
|
-
if options is not None:
|
112
|
-
# Advanced Mode
|
113
|
-
logger.debug(f"Using advanced mode with options object: {type(options).__name__}")
|
114
|
-
final_options = copy.deepcopy(options) # Prevent modification of original
|
115
|
-
found_engine = False
|
116
|
-
for name, registry_entry in self.ENGINE_REGISTRY.items():
|
117
|
-
# Check if options object is an instance of the registered options class
|
118
|
-
if isinstance(options, registry_entry["options_class"]):
|
119
|
-
selected_engine_name = name
|
120
|
-
found_engine = True
|
121
|
-
break
|
122
|
-
if not found_engine:
|
123
|
-
raise TypeError(
|
124
|
-
f"Provided options object type '{type(options).__name__}' does not match any registered engine options."
|
125
|
-
)
|
126
|
-
if kwargs:
|
127
|
-
logger.warning(
|
128
|
-
f"Keyword arguments {list(kwargs.keys())} were provided alongside 'options' and will be ignored."
|
129
|
-
)
|
130
|
-
else:
|
131
|
-
# Simple Mode
|
132
|
-
selected_engine_name = engine.lower() if engine else "easyocr" # Fallback default
|
133
|
-
logger.debug(
|
134
|
-
f"Using simple mode with engine: '{selected_engine_name}' and kwargs: {kwargs}"
|
135
|
-
)
|
136
|
-
|
137
|
-
if selected_engine_name not in self.ENGINE_REGISTRY:
|
138
|
-
raise ValueError(
|
139
|
-
f"Unknown OCR engine: '{selected_engine_name}'. Available: {list(self.ENGINE_REGISTRY.keys())}"
|
140
|
-
)
|
141
102
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
)
|
103
|
+
# --- Determine Engine ---
|
104
|
+
selected_engine_name = (engine or "easyocr").lower()
|
105
|
+
if selected_engine_name not in self.ENGINE_REGISTRY:
|
106
|
+
raise ValueError(
|
107
|
+
f"Unknown OCR engine: '{selected_engine_name}'. Available: {list(self.ENGINE_REGISTRY.keys())}"
|
108
|
+
)
|
109
|
+
logger.debug(f"Selected engine: '{selected_engine_name}'")
|
147
110
|
|
148
|
-
|
149
|
-
|
111
|
+
# --- Prepare Options ---
|
112
|
+
final_options = copy.deepcopy(options) if options is not None else None
|
150
113
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
"
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
114
|
+
# Type check options object if provided
|
115
|
+
if final_options is not None:
|
116
|
+
options_class = self.ENGINE_REGISTRY[selected_engine_name].get(
|
117
|
+
"options_class", BaseOCROptions
|
118
|
+
)
|
119
|
+
if not isinstance(final_options, options_class):
|
120
|
+
# Allow dicts to be passed directly too, assuming engine handles them
|
121
|
+
if not isinstance(final_options, dict):
|
122
|
+
raise TypeError(
|
123
|
+
f"Provided options type '{type(final_options).__name__}' is not compatible with engine '{selected_engine_name}'. Expected '{options_class.__name__}' or dict."
|
124
|
+
)
|
160
125
|
|
161
126
|
# --- Get Engine Instance and Process ---
|
162
127
|
try:
|
163
128
|
engine_instance = self._get_engine_instance(selected_engine_name)
|
164
129
|
processing_mode = "batch" if is_batch else "single image"
|
165
130
|
logger.info(f"Processing {processing_mode} with engine '{selected_engine_name}'...")
|
131
|
+
logger.debug(
|
132
|
+
f" Engine Args: languages={languages}, min_confidence={min_confidence}, device={device}, options={final_options}"
|
133
|
+
)
|
166
134
|
|
167
|
-
# Call the engine's process_image, passing
|
168
|
-
|
135
|
+
# Call the engine's process_image, passing common args and options object
|
136
|
+
# **ASSUMPTION**: Engine process_image signatures are updated to accept these common args.
|
137
|
+
results = engine_instance.process_image(
|
138
|
+
images=images,
|
139
|
+
languages=languages,
|
140
|
+
min_confidence=min_confidence,
|
141
|
+
device=device,
|
142
|
+
detect_only=detect_only,
|
143
|
+
options=final_options,
|
144
|
+
)
|
169
145
|
|
170
146
|
# Log result summary based on mode
|
171
147
|
if is_batch:
|
natural_pdf/ocr/ocr_options.py
CHANGED
@@ -14,9 +14,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
14
14
|
class BaseOCROptions:
|
15
15
|
"""Base class for OCR engine options."""
|
16
16
|
|
17
|
-
languages: List[str] = field(default_factory=lambda: ["en"])
|
18
|
-
min_confidence: float = 0.5
|
19
|
-
device: Optional[str] = "cpu" # Suggestion, actual device usage depends on engine impl.
|
20
17
|
extra_args: Dict[str, Any] = field(default_factory=dict)
|
21
18
|
|
22
19
|
|
@@ -95,12 +92,14 @@ class PaddleOCROptions(BaseOCROptions):
|
|
95
92
|
cls: Optional[bool] = None
|
96
93
|
|
97
94
|
def __post_init__(self):
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
95
|
+
pass
|
96
|
+
|
97
|
+
# if self.use_gpu is None:
|
98
|
+
# if self.device and "cuda" in self.device.lower():
|
99
|
+
# self.use_gpu = True
|
100
|
+
# else:
|
101
|
+
# self.use_gpu = False
|
102
|
+
# # logger.debug(f"Initialized PaddleOCROptions: {self}")
|
104
103
|
|
105
104
|
|
106
105
|
# --- Surya Specific Options ---
|
@@ -109,10 +108,6 @@ class SuryaOCROptions(BaseOCROptions):
|
|
109
108
|
"""Specific options for the Surya OCR engine."""
|
110
109
|
|
111
110
|
# Currently, Surya example shows languages passed at prediction time.
|
112
|
-
# Add fields here if Surya's RecognitionPredictor or DetectionPredictor
|
113
|
-
# constructors accept relevant arguments (e.g., model paths, device settings).
|
114
|
-
# For now, it primarily uses the base options like 'languages' and 'min_confidence'.
|
115
|
-
# Configuration like batch sizes are often set via environment variables for Surya.
|
116
111
|
pass
|
117
112
|
|
118
113
|
|
natural_pdf/ocr/utils.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
import io
|
2
|
+
import base64
|
3
|
+
import logging
|
4
|
+
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Any
|
5
|
+
from natural_pdf.elements.text import TextElement
|
6
|
+
from tqdm.auto import tqdm
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from natural_pdf.elements.base import Element
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def _apply_ocr_correction_to_elements(
|
15
|
+
elements: Iterable["Element"],
|
16
|
+
correction_callback: Callable[[Any], Optional[str]],
|
17
|
+
caller_info: str = "Utility",
|
18
|
+
) -> None:
|
19
|
+
"""
|
20
|
+
Applies OCR correction callback to a list of elements in place,
|
21
|
+
showing a progress bar.
|
22
|
+
|
23
|
+
Iterates through elements, checks if source starts with 'ocr', calls
|
24
|
+
the callback, and updates element.text if a new string is returned.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
elements: An iterable of Element objects.
|
28
|
+
correction_callback: A function accepting an element and returning
|
29
|
+
Optional[str] (new text or None).
|
30
|
+
caller_info: String identifying the calling context for logs.
|
31
|
+
"""
|
32
|
+
if not callable(correction_callback):
|
33
|
+
# Raise error here so individual methods don't need to repeat the check
|
34
|
+
raise TypeError("`correction_callback` must be a callable function.")
|
35
|
+
|
36
|
+
if not elements:
|
37
|
+
logger.warning(f"{caller_info}: No elements provided for correction.")
|
38
|
+
return
|
39
|
+
|
40
|
+
corrections_applied = 0
|
41
|
+
elements_checked = 0
|
42
|
+
|
43
|
+
# Prepare the iterable with tqdm
|
44
|
+
element_iterable = tqdm(elements, desc=f"Correcting OCR ({caller_info})", unit="element")
|
45
|
+
|
46
|
+
for element in element_iterable:
|
47
|
+
# Check if the element is likely from OCR and has text attribute
|
48
|
+
element_source = getattr(element, "source", None)
|
49
|
+
if (
|
50
|
+
isinstance(element_source, str)
|
51
|
+
and element_source.startswith("ocr")
|
52
|
+
and hasattr(element, "text")
|
53
|
+
):
|
54
|
+
elements_checked += 1
|
55
|
+
current_text = getattr(element, "text") # Already checked hasattr
|
56
|
+
|
57
|
+
new_text = correction_callback(element)
|
58
|
+
|
59
|
+
if new_text is not None:
|
60
|
+
if new_text != current_text:
|
61
|
+
element.text = new_text # Update in place
|
62
|
+
corrections_applied += 1
|
63
|
+
|
64
|
+
logger.info(
|
65
|
+
f"{caller_info}: OCR correction finished. Checked: {elements_checked}, Applied: {corrections_applied}"
|
66
|
+
)
|
67
|
+
# No return value needed, modifies elements in place
|
68
|
+
|
69
|
+
|
70
|
+
def direct_ocr_llm(
|
71
|
+
element,
|
72
|
+
client,
|
73
|
+
model="",
|
74
|
+
resolution=150,
|
75
|
+
prompt="OCR this image. Return only the exact text from the image. Include misspellings, punctuation, etc.",
|
76
|
+
padding=2,
|
77
|
+
) -> str:
|
78
|
+
"""Convenience method to directly OCR a region of the page."""
|
79
|
+
|
80
|
+
if isinstance(element, TextElement):
|
81
|
+
region = element.expand(left=padding, right=padding, top=padding, bottom=padding)
|
82
|
+
else:
|
83
|
+
region = element
|
84
|
+
|
85
|
+
buffered = io.BytesIO()
|
86
|
+
region_img = region.to_image(resolution=resolution, include_highlights=False)
|
87
|
+
region_img.save(buffered, format="PNG")
|
88
|
+
base64_image = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
89
|
+
|
90
|
+
response = client.chat.completions.create(
|
91
|
+
model=model,
|
92
|
+
messages=[
|
93
|
+
{
|
94
|
+
"role": "system",
|
95
|
+
"content": "You are an expert OCR engineer. You will be given an image of a region of a page. You will return the exact text from the image.",
|
96
|
+
},
|
97
|
+
{
|
98
|
+
"role": "user",
|
99
|
+
"content": [
|
100
|
+
{"type": "text", "text": prompt},
|
101
|
+
{
|
102
|
+
"type": "image_url",
|
103
|
+
"image_url": {"url": f"data:image/png;base64,{base64_image}"},
|
104
|
+
},
|
105
|
+
],
|
106
|
+
},
|
107
|
+
],
|
108
|
+
)
|
109
|
+
|
110
|
+
corrected = response.choices[0].message.content
|
111
|
+
logger.debug(f"Corrected {region.extract_text()} to {corrected}")
|
112
|
+
|
113
|
+
return corrected
|