kreuzberg 3.8.0__py3-none-any.whl → 3.8.2__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.
- kreuzberg/__init__.py +4 -0
- kreuzberg/_api/main.py +22 -1
- kreuzberg/_config.py +404 -0
- kreuzberg/_entity_extraction.py +4 -5
- kreuzberg/_extractors/_base.py +3 -5
- kreuzberg/_extractors/_image.py +18 -32
- kreuzberg/_extractors/_pandoc.py +3 -14
- kreuzberg/_extractors/_pdf.py +39 -57
- kreuzberg/_extractors/_spread_sheet.py +2 -3
- kreuzberg/_extractors/_structured.py +10 -7
- kreuzberg/_gmft.py +314 -10
- kreuzberg/_language_detection.py +1 -1
- kreuzberg/_mcp/server.py +58 -8
- kreuzberg/_ocr/__init__.py +1 -22
- kreuzberg/_ocr/_base.py +59 -0
- kreuzberg/_ocr/_easyocr.py +92 -1
- kreuzberg/_ocr/_paddleocr.py +90 -1
- kreuzberg/_ocr/_tesseract.py +556 -5
- kreuzberg/_playa.py +2 -3
- kreuzberg/_types.py +46 -24
- kreuzberg/_utils/_cache.py +35 -4
- kreuzberg/_utils/_device.py +10 -20
- kreuzberg/_utils/_errors.py +44 -45
- kreuzberg/_utils/_process_pool.py +2 -6
- kreuzberg/_utils/_quality.py +7 -11
- kreuzberg/_utils/_serialization.py +21 -16
- kreuzberg/_utils/_string.py +22 -12
- kreuzberg/_utils/_table.py +3 -4
- kreuzberg/cli.py +4 -5
- kreuzberg/exceptions.py +10 -0
- kreuzberg/extraction.py +6 -24
- kreuzberg-3.8.2.dist-info/METADATA +265 -0
- kreuzberg-3.8.2.dist-info/RECORD +53 -0
- kreuzberg/_cli_config.py +0 -175
- kreuzberg/_multiprocessing/__init__.py +0 -5
- kreuzberg/_multiprocessing/gmft_isolated.py +0 -330
- kreuzberg/_ocr/_pool.py +0 -357
- kreuzberg/_ocr/_sync.py +0 -566
- kreuzberg-3.8.0.dist-info/METADATA +0 -313
- kreuzberg-3.8.0.dist-info/RECORD +0 -57
- {kreuzberg-3.8.0.dist-info → kreuzberg-3.8.2.dist-info}/WHEEL +0 -0
- {kreuzberg-3.8.0.dist-info → kreuzberg-3.8.2.dist-info}/entry_points.txt +0 -0
- {kreuzberg-3.8.0.dist-info → kreuzberg-3.8.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,330 +0,0 @@
|
|
1
|
-
"""Isolated GMFT table extraction to handle segmentation faults."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import multiprocessing as mp
|
6
|
-
import pickle
|
7
|
-
import queue
|
8
|
-
import signal
|
9
|
-
import traceback
|
10
|
-
from typing import TYPE_CHECKING, Any
|
11
|
-
|
12
|
-
if TYPE_CHECKING:
|
13
|
-
from os import PathLike
|
14
|
-
|
15
|
-
from kreuzberg._gmft import GMFTConfig
|
16
|
-
from kreuzberg._types import TableData
|
17
|
-
|
18
|
-
|
19
|
-
def _extract_tables_in_process(
|
20
|
-
file_path: str | PathLike[str],
|
21
|
-
config_dict: dict[str, Any],
|
22
|
-
result_queue: queue.Queue[tuple[bool, Any]],
|
23
|
-
) -> None:
|
24
|
-
"""Extract tables in an isolated process to handle potential segfaults.
|
25
|
-
|
26
|
-
Args:
|
27
|
-
file_path: Path to the PDF file
|
28
|
-
config_dict: Serialized GMFTConfig as a dict
|
29
|
-
result_queue: Queue to put results or errors
|
30
|
-
"""
|
31
|
-
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
32
|
-
|
33
|
-
try:
|
34
|
-
from gmft.auto import AutoTableDetector, AutoTableFormatter # type: ignore[attr-defined]
|
35
|
-
from gmft.detectors.tatr import TATRDetectorConfig # type: ignore[attr-defined]
|
36
|
-
from gmft.formatters.tatr import TATRFormatConfig
|
37
|
-
from gmft.pdf_bindings.pdfium import PyPDFium2Document
|
38
|
-
|
39
|
-
from kreuzberg._gmft import GMFTConfig
|
40
|
-
|
41
|
-
config = GMFTConfig(**config_dict)
|
42
|
-
|
43
|
-
formatter = AutoTableFormatter( # type: ignore[no-untyped-call]
|
44
|
-
config=TATRFormatConfig(
|
45
|
-
verbosity=config.verbosity,
|
46
|
-
formatter_base_threshold=config.formatter_base_threshold,
|
47
|
-
cell_required_confidence=config.cell_required_confidence,
|
48
|
-
remove_null_rows=config.remove_null_rows,
|
49
|
-
enable_multi_header=config.enable_multi_header,
|
50
|
-
semantic_spanning_cells=config.semantic_spanning_cells,
|
51
|
-
semantic_hierarchical_left_fill=config.semantic_hierarchical_left_fill,
|
52
|
-
large_table_if_n_rows_removed=config.large_table_if_n_rows_removed,
|
53
|
-
large_table_threshold=config.large_table_threshold,
|
54
|
-
large_table_row_overlap_threshold=config.large_table_row_overlap_threshold,
|
55
|
-
large_table_maximum_rows=config.large_table_maximum_rows,
|
56
|
-
force_large_table_assumption=config.force_large_table_assumption,
|
57
|
-
)
|
58
|
-
)
|
59
|
-
detector = AutoTableDetector(config=TATRDetectorConfig(detector_base_threshold=config.detector_base_threshold)) # type: ignore[no-untyped-call]
|
60
|
-
|
61
|
-
doc = PyPDFium2Document(str(file_path))
|
62
|
-
cropped_tables = []
|
63
|
-
dataframes = []
|
64
|
-
|
65
|
-
try:
|
66
|
-
for page in doc:
|
67
|
-
cropped_tables.extend(detector.extract(page)) # type: ignore[attr-defined]
|
68
|
-
|
69
|
-
for cropped_table in cropped_tables:
|
70
|
-
formatted_table = formatter.extract(cropped_table) # type: ignore[attr-defined]
|
71
|
-
dataframes.append(formatted_table.df())
|
72
|
-
|
73
|
-
results = []
|
74
|
-
for data_frame, cropped_table in zip(dataframes, cropped_tables, strict=False):
|
75
|
-
import io
|
76
|
-
|
77
|
-
img_bytes = io.BytesIO()
|
78
|
-
cropped_image = cropped_table.image()
|
79
|
-
cropped_image.save(img_bytes, format="PNG")
|
80
|
-
img_bytes.seek(0)
|
81
|
-
|
82
|
-
results.append(
|
83
|
-
{
|
84
|
-
"cropped_image_bytes": img_bytes.getvalue(),
|
85
|
-
"page_number": cropped_table.page.page_number,
|
86
|
-
"text": data_frame.to_markdown(),
|
87
|
-
"df_pickle": pickle.dumps(data_frame),
|
88
|
-
}
|
89
|
-
)
|
90
|
-
|
91
|
-
result_queue.put((True, results))
|
92
|
-
|
93
|
-
finally:
|
94
|
-
doc.close() # type: ignore[no-untyped-call]
|
95
|
-
|
96
|
-
except Exception as e: # noqa: BLE001
|
97
|
-
error_info = {"error": str(e), "type": type(e).__name__, "traceback": traceback.format_exc()}
|
98
|
-
result_queue.put((False, error_info))
|
99
|
-
|
100
|
-
|
101
|
-
def extract_tables_isolated(
|
102
|
-
file_path: str | PathLike[str],
|
103
|
-
config: GMFTConfig | None = None,
|
104
|
-
timeout: float = 300.0,
|
105
|
-
) -> list[TableData]:
|
106
|
-
"""Extract tables using an isolated process to handle segfaults.
|
107
|
-
|
108
|
-
Args:
|
109
|
-
file_path: Path to the PDF file
|
110
|
-
config: GMFT configuration
|
111
|
-
timeout: Maximum time to wait for extraction
|
112
|
-
|
113
|
-
Returns:
|
114
|
-
List of extracted tables
|
115
|
-
|
116
|
-
Raises:
|
117
|
-
RuntimeError: If extraction fails or times out
|
118
|
-
"""
|
119
|
-
from kreuzberg._gmft import GMFTConfig
|
120
|
-
from kreuzberg._types import TableData
|
121
|
-
from kreuzberg.exceptions import ParsingError
|
122
|
-
|
123
|
-
config = config or GMFTConfig()
|
124
|
-
config_dict = config.__dict__.copy()
|
125
|
-
|
126
|
-
ctx = mp.get_context("spawn")
|
127
|
-
result_queue = ctx.Queue()
|
128
|
-
|
129
|
-
process = ctx.Process(
|
130
|
-
target=_extract_tables_in_process,
|
131
|
-
args=(str(file_path), config_dict, result_queue),
|
132
|
-
)
|
133
|
-
|
134
|
-
process.start()
|
135
|
-
|
136
|
-
try:
|
137
|
-
# Wait for result with timeout, checking for process death # ~keep
|
138
|
-
import time
|
139
|
-
|
140
|
-
start_time = time.time()
|
141
|
-
while True:
|
142
|
-
try:
|
143
|
-
success, result = result_queue.get_nowait()
|
144
|
-
break
|
145
|
-
except queue.Empty:
|
146
|
-
if time.time() - start_time > timeout:
|
147
|
-
raise
|
148
|
-
|
149
|
-
if not process.is_alive():
|
150
|
-
# Process died without putting result # ~keep
|
151
|
-
if process.exitcode == -signal.SIGSEGV:
|
152
|
-
raise ParsingError(
|
153
|
-
"GMFT process crashed with segmentation fault",
|
154
|
-
context={
|
155
|
-
"file_path": str(file_path),
|
156
|
-
"exit_code": process.exitcode,
|
157
|
-
},
|
158
|
-
) from None
|
159
|
-
raise ParsingError(
|
160
|
-
f"GMFT process died unexpectedly with exit code {process.exitcode}",
|
161
|
-
context={
|
162
|
-
"file_path": str(file_path),
|
163
|
-
"exit_code": process.exitcode,
|
164
|
-
},
|
165
|
-
) from None
|
166
|
-
|
167
|
-
time.sleep(0.1)
|
168
|
-
|
169
|
-
if success:
|
170
|
-
tables = []
|
171
|
-
for table_dict in result:
|
172
|
-
import io
|
173
|
-
import pickle
|
174
|
-
|
175
|
-
from PIL import Image
|
176
|
-
|
177
|
-
img = Image.open(io.BytesIO(table_dict["cropped_image_bytes"]))
|
178
|
-
df = pickle.loads(table_dict["df_pickle"]) # noqa: S301
|
179
|
-
|
180
|
-
tables.append(
|
181
|
-
TableData(
|
182
|
-
cropped_image=img,
|
183
|
-
page_number=table_dict["page_number"],
|
184
|
-
text=table_dict["text"],
|
185
|
-
df=df,
|
186
|
-
)
|
187
|
-
)
|
188
|
-
|
189
|
-
return tables
|
190
|
-
|
191
|
-
error_info = result
|
192
|
-
raise ParsingError(
|
193
|
-
f"GMFT table extraction failed: {error_info['error']}",
|
194
|
-
context={
|
195
|
-
"file_path": str(file_path),
|
196
|
-
"error_type": error_info["type"],
|
197
|
-
"traceback": error_info["traceback"],
|
198
|
-
},
|
199
|
-
)
|
200
|
-
|
201
|
-
except queue.Empty as e:
|
202
|
-
raise ParsingError(
|
203
|
-
"GMFT table extraction timed out",
|
204
|
-
context={
|
205
|
-
"file_path": str(file_path),
|
206
|
-
"timeout": timeout,
|
207
|
-
},
|
208
|
-
) from e
|
209
|
-
finally:
|
210
|
-
if process.is_alive():
|
211
|
-
process.terminate()
|
212
|
-
process.join(timeout=5)
|
213
|
-
if process.is_alive():
|
214
|
-
process.kill()
|
215
|
-
process.join()
|
216
|
-
|
217
|
-
|
218
|
-
async def extract_tables_isolated_async(
|
219
|
-
file_path: str | PathLike[str],
|
220
|
-
config: GMFTConfig | None = None,
|
221
|
-
timeout: float = 300.0,
|
222
|
-
) -> list[TableData]:
|
223
|
-
"""Async version of extract_tables_isolated using asyncio.
|
224
|
-
|
225
|
-
Args:
|
226
|
-
file_path: Path to the PDF file
|
227
|
-
config: GMFT configuration
|
228
|
-
timeout: Maximum time to wait for extraction
|
229
|
-
|
230
|
-
Returns:
|
231
|
-
List of extracted tables
|
232
|
-
|
233
|
-
Raises:
|
234
|
-
RuntimeError: If extraction fails or times out
|
235
|
-
"""
|
236
|
-
import anyio
|
237
|
-
|
238
|
-
from kreuzberg._gmft import GMFTConfig
|
239
|
-
from kreuzberg._types import TableData
|
240
|
-
from kreuzberg.exceptions import ParsingError
|
241
|
-
|
242
|
-
config = config or GMFTConfig()
|
243
|
-
config_dict = config.__dict__.copy()
|
244
|
-
|
245
|
-
ctx = mp.get_context("spawn")
|
246
|
-
result_queue = ctx.Queue()
|
247
|
-
|
248
|
-
process = ctx.Process(
|
249
|
-
target=_extract_tables_in_process,
|
250
|
-
args=(str(file_path), config_dict, result_queue),
|
251
|
-
)
|
252
|
-
|
253
|
-
process.start()
|
254
|
-
|
255
|
-
try:
|
256
|
-
|
257
|
-
async def wait_for_result() -> tuple[bool, Any]:
|
258
|
-
while True:
|
259
|
-
try:
|
260
|
-
return result_queue.get_nowait() # type: ignore[no-any-return]
|
261
|
-
except queue.Empty: # noqa: PERF203
|
262
|
-
await anyio.sleep(0.1)
|
263
|
-
if not process.is_alive():
|
264
|
-
# Process died without putting result # ~keep
|
265
|
-
if process.exitcode == -signal.SIGSEGV:
|
266
|
-
raise ParsingError(
|
267
|
-
"GMFT process crashed with segmentation fault",
|
268
|
-
context={
|
269
|
-
"file_path": str(file_path),
|
270
|
-
"exit_code": process.exitcode,
|
271
|
-
},
|
272
|
-
) from None
|
273
|
-
raise ParsingError(
|
274
|
-
f"GMFT process died unexpectedly with exit code {process.exitcode}",
|
275
|
-
context={
|
276
|
-
"file_path": str(file_path),
|
277
|
-
"exit_code": process.exitcode,
|
278
|
-
},
|
279
|
-
) from None
|
280
|
-
|
281
|
-
with anyio.fail_after(timeout):
|
282
|
-
success, result = await wait_for_result()
|
283
|
-
|
284
|
-
if success:
|
285
|
-
tables = []
|
286
|
-
for table_dict in result:
|
287
|
-
import io
|
288
|
-
import pickle
|
289
|
-
|
290
|
-
from PIL import Image
|
291
|
-
|
292
|
-
img = Image.open(io.BytesIO(table_dict["cropped_image_bytes"]))
|
293
|
-
df = pickle.loads(table_dict["df_pickle"]) # noqa: S301
|
294
|
-
|
295
|
-
tables.append(
|
296
|
-
TableData(
|
297
|
-
cropped_image=img,
|
298
|
-
page_number=table_dict["page_number"],
|
299
|
-
text=table_dict["text"],
|
300
|
-
df=df,
|
301
|
-
)
|
302
|
-
)
|
303
|
-
|
304
|
-
return tables
|
305
|
-
|
306
|
-
error_info = result
|
307
|
-
raise ParsingError(
|
308
|
-
f"GMFT table extraction failed: {error_info['error']}",
|
309
|
-
context={
|
310
|
-
"file_path": str(file_path),
|
311
|
-
"error_type": error_info["type"],
|
312
|
-
"traceback": error_info["traceback"],
|
313
|
-
},
|
314
|
-
)
|
315
|
-
|
316
|
-
except TimeoutError as e:
|
317
|
-
raise ParsingError(
|
318
|
-
"GMFT table extraction timed out",
|
319
|
-
context={
|
320
|
-
"file_path": str(file_path),
|
321
|
-
"timeout": timeout,
|
322
|
-
},
|
323
|
-
) from e
|
324
|
-
finally:
|
325
|
-
if process.is_alive():
|
326
|
-
process.terminate()
|
327
|
-
await anyio.to_thread.run_sync(lambda: process.join(timeout=5))
|
328
|
-
if process.is_alive():
|
329
|
-
process.kill()
|
330
|
-
await anyio.to_thread.run_sync(process.join)
|
kreuzberg/_ocr/_pool.py
DELETED
@@ -1,357 +0,0 @@
|
|
1
|
-
"""Process pools for parallel OCR processing."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
from pathlib import Path
|
6
|
-
from typing import TYPE_CHECKING, Any
|
7
|
-
|
8
|
-
from PIL import Image
|
9
|
-
from typing_extensions import Self
|
10
|
-
|
11
|
-
from kreuzberg._ocr._tesseract import TesseractConfig
|
12
|
-
from kreuzberg._types import ExtractionResult
|
13
|
-
from kreuzberg._utils._process_pool import ProcessPoolManager
|
14
|
-
|
15
|
-
if TYPE_CHECKING:
|
16
|
-
import types
|
17
|
-
|
18
|
-
|
19
|
-
def _process_image_with_tesseract(
|
20
|
-
image_path: str,
|
21
|
-
config_dict: dict[str, Any],
|
22
|
-
) -> dict[str, Any]:
|
23
|
-
"""Process a single image with Tesseract in a separate process.
|
24
|
-
|
25
|
-
This function is designed to be pickled and executed in a subprocess.
|
26
|
-
It uses direct tesseract command execution to avoid async complications.
|
27
|
-
|
28
|
-
Args:
|
29
|
-
image_path: Path to the image file.
|
30
|
-
config_dict: Tesseract configuration as dictionary.
|
31
|
-
|
32
|
-
Returns:
|
33
|
-
OCR result as dictionary.
|
34
|
-
"""
|
35
|
-
try:
|
36
|
-
import os
|
37
|
-
import subprocess
|
38
|
-
import tempfile
|
39
|
-
|
40
|
-
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp_file:
|
41
|
-
output_base = tmp_file.name.replace(".txt", "")
|
42
|
-
|
43
|
-
try:
|
44
|
-
language = config_dict.get("language", "eng")
|
45
|
-
psm = config_dict.get("psm", 3)
|
46
|
-
|
47
|
-
command = [
|
48
|
-
"tesseract",
|
49
|
-
image_path,
|
50
|
-
output_base,
|
51
|
-
"-l",
|
52
|
-
language,
|
53
|
-
"--psm",
|
54
|
-
str(psm),
|
55
|
-
"--oem",
|
56
|
-
"1",
|
57
|
-
"--loglevel",
|
58
|
-
"OFF",
|
59
|
-
]
|
60
|
-
|
61
|
-
boolean_options = [
|
62
|
-
"classify_use_pre_adapted_templates",
|
63
|
-
"language_model_ngram_on",
|
64
|
-
"tessedit_dont_blkrej_good_wds",
|
65
|
-
"tessedit_dont_rowrej_good_wds",
|
66
|
-
"tessedit_enable_dict_correction",
|
67
|
-
"tessedit_use_primary_params_model",
|
68
|
-
"textord_space_size_is_variable",
|
69
|
-
"thresholding_method",
|
70
|
-
]
|
71
|
-
|
72
|
-
for option in boolean_options:
|
73
|
-
if option in config_dict:
|
74
|
-
value = 1 if config_dict[option] else 0
|
75
|
-
command.extend(["-c", f"{option}={value}"])
|
76
|
-
|
77
|
-
env = os.environ.copy()
|
78
|
-
env["OMP_THREAD_LIMIT"] = "1"
|
79
|
-
|
80
|
-
result = subprocess.run(
|
81
|
-
command,
|
82
|
-
check=False,
|
83
|
-
env=env,
|
84
|
-
capture_output=True,
|
85
|
-
text=True,
|
86
|
-
timeout=30,
|
87
|
-
)
|
88
|
-
|
89
|
-
if result.returncode != 0:
|
90
|
-
raise Exception(f"Tesseract failed with return code {result.returncode}: {result.stderr}")
|
91
|
-
|
92
|
-
output_file = output_base + ".txt"
|
93
|
-
with Path(output_file).open(encoding="utf-8") as f:
|
94
|
-
text = f.read()
|
95
|
-
|
96
|
-
from kreuzberg._utils._string import normalize_spaces
|
97
|
-
|
98
|
-
text = normalize_spaces(text)
|
99
|
-
|
100
|
-
return {
|
101
|
-
"success": True,
|
102
|
-
"text": text,
|
103
|
-
"confidence": None,
|
104
|
-
"error": None,
|
105
|
-
}
|
106
|
-
|
107
|
-
finally:
|
108
|
-
for ext in [".txt"]:
|
109
|
-
temp_file = output_base + ext
|
110
|
-
temp_path = Path(temp_file)
|
111
|
-
if temp_path.exists():
|
112
|
-
temp_path.unlink()
|
113
|
-
|
114
|
-
except Exception as e: # noqa: BLE001
|
115
|
-
return {
|
116
|
-
"success": False,
|
117
|
-
"text": "",
|
118
|
-
"confidence": None,
|
119
|
-
"error": str(e),
|
120
|
-
}
|
121
|
-
|
122
|
-
|
123
|
-
def _process_image_bytes_with_tesseract(
|
124
|
-
image_bytes: bytes,
|
125
|
-
config_dict: dict[str, Any],
|
126
|
-
) -> dict[str, Any]:
|
127
|
-
"""Process image bytes with Tesseract in a separate process.
|
128
|
-
|
129
|
-
Args:
|
130
|
-
image_bytes: Image data as bytes.
|
131
|
-
config_dict: Tesseract configuration as dictionary.
|
132
|
-
|
133
|
-
Returns:
|
134
|
-
OCR result as dictionary.
|
135
|
-
"""
|
136
|
-
try:
|
137
|
-
import io
|
138
|
-
import tempfile
|
139
|
-
|
140
|
-
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_image:
|
141
|
-
with Image.open(io.BytesIO(image_bytes)) as image:
|
142
|
-
image.save(tmp_image.name, format="PNG")
|
143
|
-
image_path = tmp_image.name
|
144
|
-
|
145
|
-
try:
|
146
|
-
return _process_image_with_tesseract(image_path, config_dict)
|
147
|
-
finally:
|
148
|
-
image_file = Path(image_path)
|
149
|
-
if image_file.exists():
|
150
|
-
image_file.unlink()
|
151
|
-
|
152
|
-
except Exception as e: # noqa: BLE001
|
153
|
-
return {
|
154
|
-
"success": False,
|
155
|
-
"text": "",
|
156
|
-
"confidence": None,
|
157
|
-
"error": str(e),
|
158
|
-
}
|
159
|
-
|
160
|
-
|
161
|
-
class TesseractProcessPool:
|
162
|
-
"""Process pool for parallel Tesseract OCR processing."""
|
163
|
-
|
164
|
-
def __init__(
|
165
|
-
self,
|
166
|
-
config: TesseractConfig | None = None,
|
167
|
-
max_processes: int | None = None,
|
168
|
-
memory_limit_gb: float | None = None,
|
169
|
-
) -> None:
|
170
|
-
"""Initialize the Tesseract process pool.
|
171
|
-
|
172
|
-
Args:
|
173
|
-
config: Default Tesseract configuration.
|
174
|
-
max_processes: Maximum number of processes.
|
175
|
-
memory_limit_gb: Memory limit in GB.
|
176
|
-
"""
|
177
|
-
self.config = config or TesseractConfig()
|
178
|
-
self.process_manager = ProcessPoolManager(
|
179
|
-
max_processes=max_processes,
|
180
|
-
memory_limit_gb=memory_limit_gb,
|
181
|
-
)
|
182
|
-
|
183
|
-
def _config_to_dict(self, config: TesseractConfig | None = None) -> dict[str, Any]:
|
184
|
-
"""Convert TesseractConfig to dictionary for pickling."""
|
185
|
-
cfg = config or self.config
|
186
|
-
|
187
|
-
config_dict = {}
|
188
|
-
for field_name in cfg.__dataclass_fields__:
|
189
|
-
value = getattr(cfg, field_name)
|
190
|
-
|
191
|
-
if hasattr(value, "value"):
|
192
|
-
config_dict[field_name] = value.value
|
193
|
-
else:
|
194
|
-
config_dict[field_name] = value
|
195
|
-
|
196
|
-
return config_dict
|
197
|
-
|
198
|
-
def _result_from_dict(self, result_dict: dict[str, Any]) -> ExtractionResult:
|
199
|
-
"""Convert result dictionary back to OCRResult."""
|
200
|
-
if not result_dict["success"]:
|
201
|
-
from kreuzberg.exceptions import OCRError
|
202
|
-
|
203
|
-
raise OCRError(f"Tesseract processing failed: {result_dict['error']}")
|
204
|
-
|
205
|
-
from kreuzberg._mime_types import PLAIN_TEXT_MIME_TYPE
|
206
|
-
|
207
|
-
return ExtractionResult(
|
208
|
-
content=result_dict["text"],
|
209
|
-
mime_type=PLAIN_TEXT_MIME_TYPE,
|
210
|
-
metadata={"confidence": result_dict["confidence"]} if result_dict["confidence"] else {}, # type: ignore[typeddict-unknown-key]
|
211
|
-
chunks=[],
|
212
|
-
)
|
213
|
-
|
214
|
-
async def process_image(
|
215
|
-
self,
|
216
|
-
image_path: str | Path,
|
217
|
-
config: TesseractConfig | None = None,
|
218
|
-
) -> ExtractionResult:
|
219
|
-
"""Process a single image file with Tesseract.
|
220
|
-
|
221
|
-
Args:
|
222
|
-
image_path: Path to the image file.
|
223
|
-
config: Tesseract configuration (uses default if None).
|
224
|
-
|
225
|
-
Returns:
|
226
|
-
OCR result.
|
227
|
-
"""
|
228
|
-
config_dict = self._config_to_dict(config)
|
229
|
-
|
230
|
-
task_memory_mb = 80
|
231
|
-
|
232
|
-
result_dict = await self.process_manager.submit_task(
|
233
|
-
_process_image_with_tesseract,
|
234
|
-
str(image_path),
|
235
|
-
config_dict,
|
236
|
-
task_memory_mb=task_memory_mb,
|
237
|
-
)
|
238
|
-
|
239
|
-
return self._result_from_dict(result_dict)
|
240
|
-
|
241
|
-
async def process_image_bytes(
|
242
|
-
self,
|
243
|
-
image_bytes: bytes,
|
244
|
-
config: TesseractConfig | None = None,
|
245
|
-
) -> ExtractionResult:
|
246
|
-
"""Process image bytes with Tesseract.
|
247
|
-
|
248
|
-
Args:
|
249
|
-
image_bytes: Image data as bytes.
|
250
|
-
config: Tesseract configuration (uses default if None).
|
251
|
-
|
252
|
-
Returns:
|
253
|
-
OCR result.
|
254
|
-
"""
|
255
|
-
config_dict = self._config_to_dict(config)
|
256
|
-
|
257
|
-
image_size_mb = len(image_bytes) / 1024 / 1024
|
258
|
-
task_memory_mb = max(80, image_size_mb * 2 + 50)
|
259
|
-
|
260
|
-
result_dict = await self.process_manager.submit_task(
|
261
|
-
_process_image_bytes_with_tesseract,
|
262
|
-
image_bytes,
|
263
|
-
config_dict,
|
264
|
-
task_memory_mb=task_memory_mb,
|
265
|
-
)
|
266
|
-
|
267
|
-
return self._result_from_dict(result_dict)
|
268
|
-
|
269
|
-
async def process_batch_images(
|
270
|
-
self,
|
271
|
-
image_paths: list[str | Path],
|
272
|
-
config: TesseractConfig | None = None,
|
273
|
-
max_concurrent: int | None = None,
|
274
|
-
) -> list[ExtractionResult]:
|
275
|
-
"""Process a batch of images in parallel.
|
276
|
-
|
277
|
-
Args:
|
278
|
-
image_paths: List of image file paths.
|
279
|
-
config: Tesseract configuration (uses default if None).
|
280
|
-
max_concurrent: Maximum concurrent processes.
|
281
|
-
|
282
|
-
Returns:
|
283
|
-
List of OCR results in the same order as input.
|
284
|
-
"""
|
285
|
-
if not image_paths:
|
286
|
-
return []
|
287
|
-
|
288
|
-
config_dict = self._config_to_dict(config)
|
289
|
-
|
290
|
-
arg_batches = [(str(path), config_dict) for path in image_paths]
|
291
|
-
|
292
|
-
task_memory_mb = 80
|
293
|
-
|
294
|
-
result_dicts = await self.process_manager.submit_batch(
|
295
|
-
_process_image_with_tesseract,
|
296
|
-
arg_batches,
|
297
|
-
task_memory_mb=task_memory_mb,
|
298
|
-
max_concurrent=max_concurrent,
|
299
|
-
)
|
300
|
-
|
301
|
-
return [self._result_from_dict(result_dict) for result_dict in result_dicts]
|
302
|
-
|
303
|
-
async def process_batch_bytes(
|
304
|
-
self,
|
305
|
-
image_bytes_list: list[bytes],
|
306
|
-
config: TesseractConfig | None = None,
|
307
|
-
max_concurrent: int | None = None,
|
308
|
-
) -> list[ExtractionResult]:
|
309
|
-
"""Process a batch of image bytes in parallel.
|
310
|
-
|
311
|
-
Args:
|
312
|
-
image_bytes_list: List of image data as bytes.
|
313
|
-
config: Tesseract configuration (uses default if None).
|
314
|
-
max_concurrent: Maximum concurrent processes.
|
315
|
-
|
316
|
-
Returns:
|
317
|
-
List of OCR results in the same order as input.
|
318
|
-
"""
|
319
|
-
if not image_bytes_list:
|
320
|
-
return []
|
321
|
-
|
322
|
-
config_dict = self._config_to_dict(config)
|
323
|
-
|
324
|
-
arg_batches = [(image_bytes, config_dict) for image_bytes in image_bytes_list]
|
325
|
-
|
326
|
-
avg_image_size_mb = sum(len(img) for img in image_bytes_list) / len(image_bytes_list) / 1024 / 1024
|
327
|
-
task_memory_mb = max(80, avg_image_size_mb * 2 + 50)
|
328
|
-
|
329
|
-
result_dicts = await self.process_manager.submit_batch(
|
330
|
-
_process_image_bytes_with_tesseract,
|
331
|
-
arg_batches,
|
332
|
-
task_memory_mb=task_memory_mb,
|
333
|
-
max_concurrent=max_concurrent,
|
334
|
-
)
|
335
|
-
|
336
|
-
return [self._result_from_dict(result_dict) for result_dict in result_dicts]
|
337
|
-
|
338
|
-
def get_system_info(self) -> dict[str, Any]:
|
339
|
-
"""Get system information from the process manager."""
|
340
|
-
return self.process_manager.get_system_info()
|
341
|
-
|
342
|
-
def shutdown(self, wait: bool = True) -> None:
|
343
|
-
"""Shutdown the process pool."""
|
344
|
-
self.process_manager.shutdown(wait=wait)
|
345
|
-
|
346
|
-
async def __aenter__(self) -> Self:
|
347
|
-
"""Async context manager entry."""
|
348
|
-
return self
|
349
|
-
|
350
|
-
async def __aexit__(
|
351
|
-
self,
|
352
|
-
exc_type: type[BaseException] | None,
|
353
|
-
exc_val: BaseException | None,
|
354
|
-
exc_tb: types.TracebackType | None,
|
355
|
-
) -> None:
|
356
|
-
"""Async context manager exit."""
|
357
|
-
self.shutdown()
|