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.
Files changed (43) hide show
  1. kreuzberg/__init__.py +4 -0
  2. kreuzberg/_api/main.py +22 -1
  3. kreuzberg/_config.py +404 -0
  4. kreuzberg/_entity_extraction.py +4 -5
  5. kreuzberg/_extractors/_base.py +3 -5
  6. kreuzberg/_extractors/_image.py +18 -32
  7. kreuzberg/_extractors/_pandoc.py +3 -14
  8. kreuzberg/_extractors/_pdf.py +39 -57
  9. kreuzberg/_extractors/_spread_sheet.py +2 -3
  10. kreuzberg/_extractors/_structured.py +10 -7
  11. kreuzberg/_gmft.py +314 -10
  12. kreuzberg/_language_detection.py +1 -1
  13. kreuzberg/_mcp/server.py +58 -8
  14. kreuzberg/_ocr/__init__.py +1 -22
  15. kreuzberg/_ocr/_base.py +59 -0
  16. kreuzberg/_ocr/_easyocr.py +92 -1
  17. kreuzberg/_ocr/_paddleocr.py +90 -1
  18. kreuzberg/_ocr/_tesseract.py +556 -5
  19. kreuzberg/_playa.py +2 -3
  20. kreuzberg/_types.py +46 -24
  21. kreuzberg/_utils/_cache.py +35 -4
  22. kreuzberg/_utils/_device.py +10 -20
  23. kreuzberg/_utils/_errors.py +44 -45
  24. kreuzberg/_utils/_process_pool.py +2 -6
  25. kreuzberg/_utils/_quality.py +7 -11
  26. kreuzberg/_utils/_serialization.py +21 -16
  27. kreuzberg/_utils/_string.py +22 -12
  28. kreuzberg/_utils/_table.py +3 -4
  29. kreuzberg/cli.py +4 -5
  30. kreuzberg/exceptions.py +10 -0
  31. kreuzberg/extraction.py +6 -24
  32. kreuzberg-3.8.2.dist-info/METADATA +265 -0
  33. kreuzberg-3.8.2.dist-info/RECORD +53 -0
  34. kreuzberg/_cli_config.py +0 -175
  35. kreuzberg/_multiprocessing/__init__.py +0 -5
  36. kreuzberg/_multiprocessing/gmft_isolated.py +0 -330
  37. kreuzberg/_ocr/_pool.py +0 -357
  38. kreuzberg/_ocr/_sync.py +0 -566
  39. kreuzberg-3.8.0.dist-info/METADATA +0 -313
  40. kreuzberg-3.8.0.dist-info/RECORD +0 -57
  41. {kreuzberg-3.8.0.dist-info → kreuzberg-3.8.2.dist-info}/WHEEL +0 -0
  42. {kreuzberg-3.8.0.dist-info → kreuzberg-3.8.2.dist-info}/entry_points.txt +0 -0
  43. {kreuzberg-3.8.0.dist-info → kreuzberg-3.8.2.dist-info}/licenses/LICENSE +0 -0
kreuzberg/_ocr/_sync.py DELETED
@@ -1,566 +0,0 @@
1
- """Synchronous OCR implementations for all backends."""
2
-
3
- from __future__ import annotations
4
-
5
- import tempfile
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- from PIL import Image
10
-
11
- from kreuzberg._mime_types import PLAIN_TEXT_MIME_TYPE
12
- from kreuzberg._ocr._easyocr import EasyOCRConfig
13
- from kreuzberg._ocr._paddleocr import PaddleOCRConfig
14
- from kreuzberg._ocr._tesseract import TesseractConfig
15
- from kreuzberg._types import ExtractionResult
16
- from kreuzberg._utils._string import normalize_spaces
17
- from kreuzberg.exceptions import MissingDependencyError, OCRError
18
-
19
-
20
- def _get_easyocr_instance(config: EasyOCRConfig) -> Any:
21
- """Get an EasyOCR Reader instance with the given configuration."""
22
- try:
23
- import easyocr
24
- except ImportError as e:
25
- raise MissingDependencyError("EasyOCR is not installed. Install it with: pip install easyocr") from e
26
-
27
- gpu = False
28
- if hasattr(config, "device"):
29
- if config.device and config.device.lower() != "cpu":
30
- gpu = True
31
- elif hasattr(config, "use_gpu"):
32
- gpu = config.use_gpu
33
-
34
- language = config.language if hasattr(config, "language") else "en"
35
- if isinstance(language, str):
36
- lang_list = [lang.strip().lower() for lang in language.split(",")]
37
- else:
38
- lang_list = [lang.lower() for lang in language]
39
-
40
- kwargs = {
41
- "lang_list": lang_list,
42
- "gpu": gpu,
43
- "model_storage_directory": getattr(config, "model_storage_directory", None),
44
- "user_network_directory": getattr(config, "user_network_directory", None),
45
- "recog_network": getattr(config, "recog_network", None),
46
- "detector": getattr(config, "detector", None),
47
- "recognizer": getattr(config, "recognizer", None),
48
- "verbose": False,
49
- "quantize": getattr(config, "quantize", None),
50
- "cudnn_benchmark": getattr(config, "cudnn_benchmark", None),
51
- }
52
-
53
- kwargs = {k: v for k, v in kwargs.items() if v is not None}
54
-
55
- return easyocr.Reader(**kwargs)
56
-
57
-
58
- def process_image_easyocr_sync(
59
- image_path: str | Path,
60
- config: EasyOCRConfig | None = None,
61
- ) -> ExtractionResult:
62
- """Process an image with EasyOCR using pure sync implementation.
63
-
64
- This bypasses all async overhead and calls EasyOCR directly.
65
-
66
- Args:
67
- image_path: Path to the image file.
68
- config: EasyOCR configuration.
69
-
70
- Returns:
71
- Extraction result.
72
- """
73
- cfg = config or EasyOCRConfig()
74
-
75
- try:
76
- reader = _get_easyocr_instance(cfg)
77
-
78
- readtext_kwargs = {
79
- "decoder": cfg.decoder,
80
- "beamWidth": cfg.beam_width,
81
- "batch_size": getattr(cfg, "batch_size", 1),
82
- "workers": getattr(cfg, "workers", 0),
83
- "allowlist": getattr(cfg, "allowlist", None),
84
- "blocklist": getattr(cfg, "blocklist", None),
85
- "detail": getattr(cfg, "detail", 1),
86
- "rotation_info": cfg.rotation_info,
87
- "paragraph": getattr(cfg, "paragraph", False),
88
- "min_size": cfg.min_size,
89
- "text_threshold": cfg.text_threshold,
90
- "low_text": cfg.low_text,
91
- "link_threshold": cfg.link_threshold,
92
- "canvas_size": cfg.canvas_size,
93
- "mag_ratio": cfg.mag_ratio,
94
- "slope_ths": cfg.slope_ths,
95
- "ycenter_ths": cfg.ycenter_ths,
96
- "height_ths": cfg.height_ths,
97
- "width_ths": cfg.width_ths,
98
- "add_margin": cfg.add_margin,
99
- "x_ths": cfg.x_ths,
100
- "y_ths": cfg.y_ths,
101
- }
102
-
103
- readtext_kwargs = {k: v for k, v in readtext_kwargs.items() if v is not None}
104
-
105
- results = reader.readtext(str(image_path), **readtext_kwargs)
106
-
107
- if not results:
108
- return ExtractionResult(
109
- content="",
110
- mime_type=PLAIN_TEXT_MIME_TYPE,
111
- metadata={},
112
- chunks=[],
113
- )
114
-
115
- texts = []
116
- confidences = []
117
-
118
- detail_value = getattr(cfg, "detail", 1)
119
- if detail_value:
120
- for result in results:
121
- min_result_length = 2
122
- max_confidence_index = 2
123
- if len(result) >= min_result_length:
124
- _bbox, text = result[0], result[1]
125
- confidence = result[max_confidence_index] if len(result) > max_confidence_index else 1.0
126
- texts.append(text)
127
- confidences.append(confidence)
128
- else:
129
- texts = results
130
- confidences = [1.0] * len(texts)
131
-
132
- content = "\n".join(texts)
133
- content = normalize_spaces(content)
134
-
135
- avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0
136
-
137
- metadata = {"confidence": avg_confidence} if confidences else {}
138
-
139
- return ExtractionResult(
140
- content=content,
141
- mime_type=PLAIN_TEXT_MIME_TYPE,
142
- metadata=metadata, # type: ignore[arg-type]
143
- chunks=[],
144
- )
145
-
146
- except Exception as e:
147
- raise OCRError(f"EasyOCR processing failed: {e}") from e
148
-
149
-
150
- def process_image_bytes_easyocr_sync(
151
- image_bytes: bytes,
152
- config: EasyOCRConfig | None = None,
153
- ) -> ExtractionResult:
154
- """Process image bytes with EasyOCR using pure sync implementation.
155
-
156
- Args:
157
- image_bytes: Image data as bytes.
158
- config: EasyOCR configuration.
159
-
160
- Returns:
161
- Extraction result.
162
- """
163
- import io
164
-
165
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_image:
166
- with Image.open(io.BytesIO(image_bytes)) as image:
167
- image.save(tmp_image.name, format="PNG")
168
- image_path = tmp_image.name
169
-
170
- try:
171
- return process_image_easyocr_sync(image_path, config)
172
- finally:
173
- image_file = Path(image_path)
174
- if image_file.exists():
175
- image_file.unlink()
176
-
177
-
178
- def _get_paddleocr_instance(config: PaddleOCRConfig) -> Any:
179
- """Get a PaddleOCR instance with the given configuration."""
180
- try:
181
- import paddleocr
182
- except ImportError as e:
183
- raise MissingDependencyError("PaddleOCR is not installed. Install it with: pip install paddleocr") from e
184
-
185
- if hasattr(config, "device"):
186
- if config.device and config.device.lower() != "cpu":
187
- pass
188
- elif hasattr(config, "use_gpu"):
189
- pass
190
-
191
- kwargs = {
192
- "lang": config.language,
193
- "use_textline_orientation": config.use_angle_cls,
194
- }
195
-
196
- if hasattr(config, "det_db_thresh"):
197
- kwargs["text_det_thresh"] = config.det_db_thresh
198
- if hasattr(config, "det_db_box_thresh"):
199
- kwargs["text_det_box_thresh"] = config.det_db_box_thresh
200
- if hasattr(config, "det_db_unclip_ratio"):
201
- kwargs["text_det_unclip_ratio"] = config.det_db_unclip_ratio
202
- if hasattr(config, "det_max_side_len"):
203
- kwargs["text_det_limit_side_len"] = config.det_max_side_len
204
- if hasattr(config, "drop_score"):
205
- kwargs["text_rec_score_thresh"] = config.drop_score
206
-
207
- return paddleocr.PaddleOCR(**kwargs)
208
-
209
-
210
- def process_image_paddleocr_sync(
211
- image_path: str | Path,
212
- config: PaddleOCRConfig | None = None,
213
- ) -> ExtractionResult:
214
- """Process an image with PaddleOCR using pure sync implementation.
215
-
216
- This bypasses all async overhead and calls PaddleOCR directly.
217
-
218
- Args:
219
- image_path: Path to the image file.
220
- config: PaddleOCR configuration.
221
-
222
- Returns:
223
- Extraction result.
224
- """
225
- cfg = config or PaddleOCRConfig()
226
-
227
- try:
228
- ocr_instance = _get_paddleocr_instance(cfg)
229
-
230
- results = ocr_instance.ocr(str(image_path))
231
-
232
- if not results or not results[0]:
233
- return ExtractionResult(
234
- content="",
235
- mime_type=PLAIN_TEXT_MIME_TYPE,
236
- metadata={},
237
- chunks=[],
238
- )
239
-
240
- ocr_result = results[0]
241
- result_data = ocr_result.json["res"]
242
-
243
- texts = result_data.get("rec_texts", [])
244
- scores = result_data.get("rec_scores", [])
245
-
246
- if not texts:
247
- return ExtractionResult(
248
- content="",
249
- mime_type=PLAIN_TEXT_MIME_TYPE,
250
- metadata={},
251
- chunks=[],
252
- )
253
-
254
- content = "\n".join(texts)
255
- content = normalize_spaces(content)
256
-
257
- avg_confidence = sum(scores) / len(scores) if scores else 0.0
258
-
259
- metadata = {"confidence": avg_confidence} if scores else {}
260
-
261
- return ExtractionResult(
262
- content=content,
263
- mime_type=PLAIN_TEXT_MIME_TYPE,
264
- metadata=metadata, # type: ignore[arg-type]
265
- chunks=[],
266
- )
267
-
268
- except Exception as e:
269
- raise OCRError(f"PaddleOCR processing failed: {e}") from e
270
-
271
-
272
- def process_image_bytes_paddleocr_sync(
273
- image_bytes: bytes,
274
- config: PaddleOCRConfig | None = None,
275
- ) -> ExtractionResult:
276
- """Process image bytes with PaddleOCR using pure sync implementation.
277
-
278
- Args:
279
- image_bytes: Image data as bytes.
280
- config: PaddleOCR configuration.
281
-
282
- Returns:
283
- Extraction result.
284
- """
285
- import io
286
-
287
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_image:
288
- with Image.open(io.BytesIO(image_bytes)) as image:
289
- image.save(tmp_image.name, format="PNG")
290
- image_path = tmp_image.name
291
-
292
- try:
293
- return process_image_paddleocr_sync(image_path, config)
294
- finally:
295
- image_file = Path(image_path)
296
- if image_file.exists():
297
- image_file.unlink()
298
-
299
-
300
- def process_image_tesseract_sync(
301
- image_path: str | Path,
302
- config: TesseractConfig | None = None,
303
- ) -> ExtractionResult:
304
- """Process an image with Tesseract using pure sync implementation.
305
-
306
- This bypasses all async overhead and calls Tesseract directly.
307
-
308
- Args:
309
- image_path: Path to the image file.
310
- config: Tesseract configuration.
311
-
312
- Returns:
313
- Extraction result.
314
- """
315
- import os
316
- import subprocess
317
-
318
- cfg = config or TesseractConfig()
319
-
320
- with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp_file:
321
- output_base = tmp_file.name.replace(".txt", "")
322
-
323
- try:
324
- command = [
325
- "tesseract",
326
- str(image_path),
327
- output_base,
328
- "-l",
329
- cfg.language,
330
- "--psm",
331
- str(cfg.psm.value if hasattr(cfg.psm, "value") else cfg.psm),
332
- "--oem",
333
- "1",
334
- "--loglevel",
335
- "OFF",
336
- ]
337
-
338
- boolean_fields = [
339
- "classify_use_pre_adapted_templates",
340
- "language_model_ngram_on",
341
- "tessedit_dont_blkrej_good_wds",
342
- "tessedit_dont_rowrej_good_wds",
343
- "tessedit_enable_dict_correction",
344
- "tessedit_use_primary_params_model",
345
- "textord_space_size_is_variable",
346
- "thresholding_method",
347
- ]
348
-
349
- for field in boolean_fields:
350
- if hasattr(cfg, field):
351
- value = 1 if getattr(cfg, field) else 0
352
- command.extend(["-c", f"{field}={value}"])
353
-
354
- env = os.environ.copy()
355
- env["OMP_THREAD_LIMIT"] = "1"
356
-
357
- result = subprocess.run(
358
- command,
359
- check=False,
360
- env=env,
361
- capture_output=True,
362
- text=True,
363
- timeout=30,
364
- )
365
-
366
- if result.returncode != 0:
367
- raise OCRError(f"Tesseract failed with return code {result.returncode}: {result.stderr}")
368
-
369
- output_file = output_base + ".txt"
370
- with Path(output_file).open(encoding="utf-8") as f:
371
- text = f.read()
372
-
373
- text = normalize_spaces(text)
374
-
375
- return ExtractionResult(
376
- content=text,
377
- mime_type=PLAIN_TEXT_MIME_TYPE,
378
- metadata={},
379
- chunks=[],
380
- )
381
-
382
- finally:
383
- for ext in [".txt"]:
384
- temp_file = output_base + ext
385
- temp_path = Path(temp_file)
386
- if temp_path.exists():
387
- temp_path.unlink()
388
-
389
-
390
- def process_image_bytes_tesseract_sync(
391
- image_bytes: bytes,
392
- config: TesseractConfig | None = None,
393
- ) -> ExtractionResult:
394
- """Process image bytes with Tesseract using pure sync implementation.
395
-
396
- Args:
397
- image_bytes: Image data as bytes.
398
- config: Tesseract configuration.
399
-
400
- Returns:
401
- Extraction result.
402
- """
403
- import io
404
-
405
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_image:
406
- with Image.open(io.BytesIO(image_bytes)) as image:
407
- image.save(tmp_image.name, format="PNG")
408
- image_path = tmp_image.name
409
-
410
- try:
411
- return process_image_tesseract_sync(image_path, config)
412
- finally:
413
- image_file = Path(image_path)
414
- if image_file.exists():
415
- image_file.unlink()
416
-
417
-
418
- def process_batch_images_sync(
419
- image_paths: list[str | Path],
420
- config: EasyOCRConfig | PaddleOCRConfig | TesseractConfig | None = None,
421
- backend: str = "tesseract",
422
- ) -> list[ExtractionResult]:
423
- """Process a batch of images sequentially with pure sync implementation.
424
-
425
- Args:
426
- image_paths: List of image file paths.
427
- config: OCR configuration.
428
- backend: OCR backend to use.
429
-
430
- Returns:
431
- List of extraction results.
432
- """
433
- results = []
434
- for image_path in image_paths:
435
- if backend == "easyocr":
436
- result = process_image_easyocr_sync(image_path, config) # type: ignore[arg-type]
437
- elif backend == "paddleocr":
438
- result = process_image_paddleocr_sync(image_path, config) # type: ignore[arg-type]
439
- else:
440
- result = process_image_tesseract_sync(image_path, config) # type: ignore[arg-type]
441
- results.append(result)
442
- return results
443
-
444
-
445
- def process_batch_images_threaded(
446
- image_paths: list[str | Path],
447
- config: EasyOCRConfig | PaddleOCRConfig | TesseractConfig | None = None,
448
- backend: str = "tesseract",
449
- max_workers: int | None = None,
450
- ) -> list[ExtractionResult]:
451
- """Process a batch of images using threading.
452
-
453
- Args:
454
- image_paths: List of image file paths.
455
- config: OCR configuration.
456
- backend: OCR backend to use.
457
- max_workers: Maximum number of threads.
458
-
459
- Returns:
460
- List of extraction results in same order as input.
461
- """
462
- import multiprocessing as mp
463
- from concurrent.futures import ThreadPoolExecutor, as_completed
464
-
465
- if max_workers is None:
466
- max_workers = min(len(image_paths), mp.cpu_count())
467
-
468
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
469
- if backend == "easyocr":
470
- future_to_index = {
471
- executor.submit(process_image_easyocr_sync, path, config): i # type: ignore[arg-type]
472
- for i, path in enumerate(image_paths)
473
- }
474
- elif backend == "paddleocr":
475
- future_to_index = {
476
- executor.submit(process_image_paddleocr_sync, path, config): i # type: ignore[arg-type]
477
- for i, path in enumerate(image_paths)
478
- }
479
- else:
480
- future_to_index = {
481
- executor.submit(process_image_tesseract_sync, path, config): i # type: ignore[arg-type]
482
- for i, path in enumerate(image_paths)
483
- }
484
-
485
- results: list[ExtractionResult] = [None] * len(image_paths) # type: ignore[list-item]
486
- for future in as_completed(future_to_index):
487
- index = future_to_index[future]
488
- try:
489
- results[index] = future.result()
490
- except Exception as e: # noqa: BLE001
491
- results[index] = ExtractionResult(
492
- content=f"Error: {e}",
493
- mime_type=PLAIN_TEXT_MIME_TYPE,
494
- metadata={"error": str(e)}, # type: ignore[typeddict-unknown-key]
495
- chunks=[],
496
- )
497
-
498
- return results
499
-
500
-
501
- def process_batch_images_process_pool(
502
- image_paths: list[str | Path],
503
- config: TesseractConfig | None = None,
504
- max_workers: int | None = None,
505
- ) -> list[ExtractionResult]:
506
- """Process a batch of images using process pool.
507
-
508
- Args:
509
- image_paths: List of image file paths.
510
- config: Tesseract configuration.
511
- max_workers: Maximum number of processes.
512
-
513
- Returns:
514
- List of extraction results in same order as input.
515
- """
516
- import multiprocessing as mp
517
- from concurrent.futures import ProcessPoolExecutor, as_completed
518
-
519
- if max_workers is None:
520
- max_workers = min(len(image_paths), mp.cpu_count())
521
-
522
- cfg = config or TesseractConfig()
523
- config_dict = {}
524
- for field_name in cfg.__dataclass_fields__:
525
- value = getattr(cfg, field_name)
526
- if hasattr(value, "value"):
527
- config_dict[field_name] = value.value
528
- else:
529
- config_dict[field_name] = value
530
-
531
- with ProcessPoolExecutor(max_workers=max_workers) as executor:
532
- from kreuzberg._ocr._pool import _process_image_with_tesseract
533
-
534
- future_to_index = {
535
- executor.submit(_process_image_with_tesseract, str(path), config_dict): i
536
- for i, path in enumerate(image_paths)
537
- }
538
-
539
- results: list[ExtractionResult] = [None] * len(image_paths) # type: ignore[list-item]
540
- for future in as_completed(future_to_index):
541
- index = future_to_index[future]
542
- try:
543
- result_dict = future.result()
544
- if result_dict["success"]:
545
- results[index] = ExtractionResult(
546
- content=result_dict["text"],
547
- mime_type=PLAIN_TEXT_MIME_TYPE,
548
- metadata={},
549
- chunks=[],
550
- )
551
- else:
552
- results[index] = ExtractionResult(
553
- content=f"Error: {result_dict['error']}",
554
- mime_type=PLAIN_TEXT_MIME_TYPE,
555
- metadata={"error": result_dict["error"]}, # type: ignore[typeddict-unknown-key]
556
- chunks=[],
557
- )
558
- except Exception as e: # noqa: BLE001
559
- results[index] = ExtractionResult(
560
- content=f"Error: {e}",
561
- mime_type=PLAIN_TEXT_MIME_TYPE,
562
- metadata={"error": str(e)}, # type: ignore[typeddict-unknown-key]
563
- chunks=[],
564
- )
565
-
566
- return results