kreuzberg 1.1.0__py3-none-any.whl → 1.2.0__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/_extractors.py +87 -7
- kreuzberg/_mime_types.py +2 -2
- kreuzberg/_string.py +12 -0
- kreuzberg/extraction.py +17 -0
- {kreuzberg-1.1.0.dist-info → kreuzberg-1.2.0.dist-info}/METADATA +27 -12
- kreuzberg-1.2.0.dist-info/RECORD +13 -0
- kreuzberg-1.1.0.dist-info/RECORD +0 -13
- {kreuzberg-1.1.0.dist-info → kreuzberg-1.2.0.dist-info}/LICENSE +0 -0
- {kreuzberg-1.1.0.dist-info → kreuzberg-1.2.0.dist-info}/WHEEL +0 -0
- {kreuzberg-1.1.0.dist-info → kreuzberg-1.2.0.dist-info}/top_level.txt +0 -0
kreuzberg/_extractors.py
CHANGED
@@ -1,13 +1,21 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import re
|
4
|
+
from contextlib import suppress
|
5
|
+
from html import escape
|
6
|
+
from io import BytesIO
|
3
7
|
from typing import TYPE_CHECKING, cast
|
4
8
|
|
9
|
+
from anyio import Path as AsyncPath
|
5
10
|
from charset_normalizer import detect
|
11
|
+
from pptx import Presentation
|
12
|
+
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
6
13
|
from pypandoc import convert_file, convert_text
|
7
14
|
from pypdfium2 import PdfDocument, PdfiumError
|
8
15
|
from pytesseract import TesseractError, image_to_string
|
9
16
|
|
10
17
|
from kreuzberg._mime_types import PANDOC_MIME_TYPE_EXT_MAP
|
18
|
+
from kreuzberg._string import normalize_spaces
|
11
19
|
from kreuzberg._sync import run_sync
|
12
20
|
from kreuzberg.exceptions import ParsingError
|
13
21
|
|
@@ -33,7 +41,7 @@ def _extract_pdf_with_tesseract(file_path: Path) -> str:
|
|
33
41
|
images = [page.render(scale=2.0).to_pil() for page in pdf]
|
34
42
|
|
35
43
|
text = "\n".join(image_to_string(img) for img in images)
|
36
|
-
return text
|
44
|
+
return normalize_spaces(text)
|
37
45
|
except (PdfiumError, TesseractError) as e:
|
38
46
|
# TODO: add test case
|
39
47
|
raise ParsingError(
|
@@ -56,7 +64,7 @@ def _extract_pdf_with_pdfium2(file_path: Path) -> str:
|
|
56
64
|
try:
|
57
65
|
document = PdfDocument(file_path)
|
58
66
|
text = "\n".join(page.get_textpage().get_text_range() for page in document)
|
59
|
-
return text
|
67
|
+
return normalize_spaces(text)
|
60
68
|
except PdfiumError as e:
|
61
69
|
# TODO: add test case
|
62
70
|
raise ParsingError(
|
@@ -75,9 +83,9 @@ async def _extract_pdf_file(file_path: Path, force_ocr: bool = False) -> str:
|
|
75
83
|
The extracted text.
|
76
84
|
"""
|
77
85
|
if not force_ocr and (content := await run_sync(_extract_pdf_with_pdfium2, file_path)):
|
78
|
-
return content
|
86
|
+
return normalize_spaces(content)
|
79
87
|
|
80
|
-
return await run_sync(_extract_pdf_with_tesseract, file_path)
|
88
|
+
return normalize_spaces(await run_sync(_extract_pdf_with_tesseract, file_path))
|
81
89
|
|
82
90
|
|
83
91
|
async def _extract_content_with_pandoc(file_data: bytes, mime_type: str, encoding: str | None = None) -> str:
|
@@ -97,7 +105,9 @@ async def _extract_content_with_pandoc(file_data: bytes, mime_type: str, encodin
|
|
97
105
|
ext = PANDOC_MIME_TYPE_EXT_MAP[mime_type]
|
98
106
|
encoding = encoding or detect(file_data)["encoding"] or "utf-8"
|
99
107
|
try:
|
100
|
-
return
|
108
|
+
return normalize_spaces(
|
109
|
+
cast(str, await run_sync(convert_text, file_data, to="md", format=ext, encoding=encoding))
|
110
|
+
)
|
101
111
|
except RuntimeError as e:
|
102
112
|
# TODO: add test case
|
103
113
|
raise ParsingError(
|
@@ -121,7 +131,7 @@ async def _extract_file_with_pandoc(file_path: Path | str, mime_type: str) -> st
|
|
121
131
|
"""
|
122
132
|
ext = PANDOC_MIME_TYPE_EXT_MAP[mime_type]
|
123
133
|
try:
|
124
|
-
return cast(str, await run_sync(convert_file, file_path, to="md", format=ext))
|
134
|
+
return normalize_spaces(cast(str, await run_sync(convert_file, file_path, to="md", format=ext)))
|
125
135
|
except RuntimeError as e:
|
126
136
|
raise ParsingError(
|
127
137
|
f"Could not extract text from {PANDOC_MIME_TYPE_EXT_MAP[mime_type]} file",
|
@@ -142,8 +152,78 @@ async def _extract_image_with_tesseract(file_path: Path | str) -> str:
|
|
142
152
|
The extracted content.
|
143
153
|
"""
|
144
154
|
try:
|
145
|
-
return cast(str, image_to_string(str(file_path))
|
155
|
+
return normalize_spaces(cast(str, image_to_string(str(file_path))))
|
146
156
|
except TesseractError as e:
|
147
157
|
raise ParsingError(
|
148
158
|
"Could not extract text from image file", context={"file_path": str(file_path), "error": str(e)}
|
149
159
|
) from e
|
160
|
+
|
161
|
+
|
162
|
+
async def _extract_pptx_file(file_path_or_contents: Path | bytes) -> str:
|
163
|
+
"""Extract text from a PPTX file.
|
164
|
+
|
165
|
+
Notes:
|
166
|
+
This function is based on code vendored from `markitdown`, which has an MIT license as well.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
file_path_or_contents: The path to the PPTX file or its contents as bytes.
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
The extracted text content
|
173
|
+
"""
|
174
|
+
md_content = ""
|
175
|
+
file_contents = (
|
176
|
+
file_path_or_contents
|
177
|
+
if isinstance(file_path_or_contents, bytes)
|
178
|
+
else await AsyncPath(file_path_or_contents).read_bytes()
|
179
|
+
)
|
180
|
+
presentation = Presentation(BytesIO(file_contents))
|
181
|
+
|
182
|
+
for index, slide in enumerate(presentation.slides):
|
183
|
+
md_content += f"\n\n<!-- Slide number: {index + 1} -->\n"
|
184
|
+
|
185
|
+
title = slide.shapes.title
|
186
|
+
|
187
|
+
for shape in slide.shapes:
|
188
|
+
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE or (
|
189
|
+
shape.shape_type == MSO_SHAPE_TYPE.PLACEHOLDER and hasattr(shape, "image")
|
190
|
+
):
|
191
|
+
alt_text = ""
|
192
|
+
with suppress(AttributeError):
|
193
|
+
# access non-visual properties
|
194
|
+
alt_text = shape._element._nvXxPr.cNvPr.attrib.get("descr", "") # noqa: SLF001
|
195
|
+
|
196
|
+
filename = re.sub(r"\W", "", shape.name) + ".jpg"
|
197
|
+
md_content += f"\n\n"
|
198
|
+
|
199
|
+
elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:
|
200
|
+
html_table = "<table>"
|
201
|
+
first_row = True
|
202
|
+
|
203
|
+
for row in shape.table.rows:
|
204
|
+
html_table += "<tr>"
|
205
|
+
|
206
|
+
for cell in row.cells:
|
207
|
+
tag = "th" if first_row else "td"
|
208
|
+
html_table += f"<{tag}>{escape(cell.text)}</{tag}>"
|
209
|
+
|
210
|
+
html_table += "</tr>"
|
211
|
+
first_row = False
|
212
|
+
|
213
|
+
html_table += "</table>"
|
214
|
+
md_content += "\n" + html_table + "\n"
|
215
|
+
|
216
|
+
elif shape.has_text_frame:
|
217
|
+
md_content += "# " + shape.text.lstrip() + "\n" if shape == title else shape.text + "\n"
|
218
|
+
|
219
|
+
md_content = md_content.strip()
|
220
|
+
if slide.has_notes_slide:
|
221
|
+
md_content += "\n\n### Notes:\n"
|
222
|
+
notes_frame = slide.notes_slide.notes_text_frame
|
223
|
+
|
224
|
+
if notes_frame is not None:
|
225
|
+
md_content += notes_frame.text
|
226
|
+
|
227
|
+
md_content = md_content.strip()
|
228
|
+
|
229
|
+
return normalize_spaces(md_content)
|
kreuzberg/_mime_types.py
CHANGED
@@ -8,7 +8,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
8
8
|
MARKDOWN_MIME_TYPE: Final[str] = "text/markdown"
|
9
9
|
PLAIN_TEXT_MIME_TYPE: Final[str] = "text/plain"
|
10
10
|
PDF_MIME_TYPE: Final[str] = "application/pdf"
|
11
|
-
|
11
|
+
POWER_POINT_MIME_TYPE: Final[str] = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
12
12
|
PLAIN_TEXT_MIME_TYPES: Final[set[str]] = {PLAIN_TEXT_MIME_TYPE, MARKDOWN_MIME_TYPE}
|
13
13
|
|
14
14
|
IMAGE_MIME_TYPES: Final[set[str]] = {
|
@@ -93,5 +93,5 @@ PANDOC_MIME_TYPE_EXT_MAP: Final[Mapping[str, str]] = {
|
|
93
93
|
}
|
94
94
|
|
95
95
|
SUPPORTED_MIME_TYPES: Final[set[str]] = (
|
96
|
-
PLAIN_TEXT_MIME_TYPES | IMAGE_MIME_TYPES | PANDOC_SUPPORTED_MIME_TYPES | {PDF_MIME_TYPE}
|
96
|
+
PLAIN_TEXT_MIME_TYPES | IMAGE_MIME_TYPES | PANDOC_SUPPORTED_MIME_TYPES | {PDF_MIME_TYPE, POWER_POINT_MIME_TYPE}
|
97
97
|
)
|
kreuzberg/_string.py
CHANGED
@@ -33,3 +33,15 @@ def safe_decode(byte_data: bytes, encoding: str | None = None) -> str:
|
|
33
33
|
|
34
34
|
# TODO: add test case
|
35
35
|
return byte_data.decode("latin-1", errors="replace")
|
36
|
+
|
37
|
+
|
38
|
+
def normalize_spaces(text: str) -> str:
|
39
|
+
"""Normalize the spaces in a string.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
text: The text to sanitize.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
The sanitized text.
|
46
|
+
"""
|
47
|
+
return " ".join(text.strip().split())
|
kreuzberg/extraction.py
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
"""This module provides functions to extract textual content from files.
|
2
|
+
|
3
|
+
It includes vendored code:
|
4
|
+
|
5
|
+
- The extract PPTX logic is based on code vendored from `markitdown` to extract text from PPTX files.
|
6
|
+
See: https://github.com/microsoft/markitdown/blob/main/src/markitdown/_markitdown.py
|
7
|
+
Refer to the markitdown repository for it's license (MIT).
|
8
|
+
"""
|
9
|
+
|
1
10
|
from __future__ import annotations
|
2
11
|
|
3
12
|
from mimetypes import guess_type
|
@@ -12,6 +21,7 @@ from kreuzberg._extractors import (
|
|
12
21
|
_extract_file_with_pandoc,
|
13
22
|
_extract_image_with_tesseract,
|
14
23
|
_extract_pdf_file,
|
24
|
+
_extract_pptx_file,
|
15
25
|
)
|
16
26
|
from kreuzberg._mime_types import (
|
17
27
|
IMAGE_MIME_TYPE_EXT_MAP,
|
@@ -20,6 +30,7 @@ from kreuzberg._mime_types import (
|
|
20
30
|
PANDOC_SUPPORTED_MIME_TYPES,
|
21
31
|
PDF_MIME_TYPE,
|
22
32
|
PLAIN_TEXT_MIME_TYPE,
|
33
|
+
POWER_POINT_MIME_TYPE,
|
23
34
|
SUPPORTED_MIME_TYPES,
|
24
35
|
)
|
25
36
|
from kreuzberg._string import safe_decode
|
@@ -76,6 +87,9 @@ async def extract_bytes(content: bytes, mime_type: str, force_ocr: bool = False)
|
|
76
87
|
content=await _extract_content_with_pandoc(content, mime_type), mime_type=MARKDOWN_MIME_TYPE
|
77
88
|
)
|
78
89
|
|
90
|
+
if mime_type == POWER_POINT_MIME_TYPE or mime_type.startswith(POWER_POINT_MIME_TYPE):
|
91
|
+
return ExtractionResult(content=await _extract_pptx_file(content), mime_type=MARKDOWN_MIME_TYPE)
|
92
|
+
|
79
93
|
return ExtractionResult(
|
80
94
|
content=safe_decode(content),
|
81
95
|
mime_type=mime_type,
|
@@ -125,4 +139,7 @@ async def extract_file(
|
|
125
139
|
content=await _extract_file_with_pandoc(file_path, mime_type), mime_type=MARKDOWN_MIME_TYPE
|
126
140
|
)
|
127
141
|
|
142
|
+
if mime_type == POWER_POINT_MIME_TYPE or mime_type.startswith(POWER_POINT_MIME_TYPE):
|
143
|
+
return ExtractionResult(content=await _extract_pptx_file(file_path), mime_type=MARKDOWN_MIME_TYPE)
|
144
|
+
|
128
145
|
return ExtractionResult(content=await AsyncPath(file_path).read_text(), mime_type=mime_type)
|
@@ -1,11 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: kreuzberg
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
4
4
|
Summary: A text extraction library supporting PDFs, images, office documents and more
|
5
5
|
Author-email: Na'aman Hirschfeld <nhirschfed@gmail.com>
|
6
6
|
License: MIT
|
7
7
|
Project-URL: homepage, https://github.com/Goldziher/kreuzberg
|
8
|
-
Keywords:
|
8
|
+
Keywords: document-processing,docx,image-to-text,latex,markdown,ocr,odt,office-documents,pandoc,pdf,pdf-extraction,rag,text-extraction,text-processing
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
10
10
|
Classifier: Intended Audience :: Developers
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -28,6 +28,7 @@ Requires-Dist: charset-normalizer>=3.4.1
|
|
28
28
|
Requires-Dist: pypandoc>=1.15
|
29
29
|
Requires-Dist: pypdfium2>=4.30.1
|
30
30
|
Requires-Dist: pytesseract>=0.3.13
|
31
|
+
Requires-Dist: python-pptx>=1.0.2
|
31
32
|
Requires-Dist: typing-extensions>=4.12.2
|
32
33
|
|
33
34
|
# Kreuzberg
|
@@ -65,6 +66,28 @@ Hence, this library.
|
|
65
66
|
- [pandoc](https://pandoc.org/installing.html) (non-pdf text extraction, GPL v2.0 licensed but used via CLI only)
|
66
67
|
- [tesseract-ocr](https://tesseract-ocr.github.io/) (for image/PDF OCR, Apache License)
|
67
68
|
|
69
|
+
## Dependencies and Philosophy
|
70
|
+
|
71
|
+
This library is built to be minimalist and simple. It also aims to utilize OSS tools for the job. Its fundamentally a
|
72
|
+
high order async abstraction on top of other tools, think of it like the library you would bake in your code base, but
|
73
|
+
polished and well maintained.
|
74
|
+
|
75
|
+
### Dependencies
|
76
|
+
|
77
|
+
- PDFs are processed using pdfium2 for searchable PDFs + Tesseract OCR for scanned documents
|
78
|
+
- Images are processed using Tesseract OCR
|
79
|
+
- Office documents and other formats are processed using Pandoc, or python-pptx for PPTX files
|
80
|
+
- Plain text files are read directly with appropriate encoding detection
|
81
|
+
|
82
|
+
### Roadmap
|
83
|
+
|
84
|
+
[] - extra install groups (to make dependencies optional and offer alternatives)
|
85
|
+
[] - html file text extraction
|
86
|
+
[] - better PDF table extraction
|
87
|
+
[] - metadata extraction
|
88
|
+
|
89
|
+
Feel free to open a discussion in GitHub or an issue if you have any feature requests, but keep the philosophy part in mind
|
90
|
+
|
68
91
|
## Supported File Types
|
69
92
|
|
70
93
|
Kreuzberg supports a wide range of file formats:
|
@@ -72,7 +95,8 @@ Kreuzberg supports a wide range of file formats:
|
|
72
95
|
### Document Formats
|
73
96
|
|
74
97
|
- PDF (`.pdf`) - both searchable and scanned documents
|
75
|
-
- Word Documents (`.docx`)
|
98
|
+
- Word Documents (`.docx`, `.doc`)
|
99
|
+
- Power Point Presentations (`.pptx`)
|
76
100
|
- OpenDocument Text (`.odt`)
|
77
101
|
- Rich Text Format (`.rtf`)
|
78
102
|
|
@@ -102,13 +126,6 @@ Kreuzberg supports a wide range of file formats:
|
|
102
126
|
- Comma-Separated Values (`.csv`)
|
103
127
|
- Tab-Separated Values (`.tsv`)
|
104
128
|
|
105
|
-
All formats support text extraction, with different processing methods:
|
106
|
-
|
107
|
-
- PDFs are processed using pdfium2 for searchable PDFs and Tesseract OCR for scanned documents
|
108
|
-
- Images are processed using Tesseract OCR
|
109
|
-
- Office documents and other formats are processed using Pandoc
|
110
|
-
- Plain text files are read directly with appropriate encoding detection
|
111
|
-
|
112
129
|
## Usage
|
113
130
|
|
114
131
|
Kreuzberg exports two async functions:
|
@@ -116,8 +133,6 @@ Kreuzberg exports two async functions:
|
|
116
133
|
- Extract text from a file (string path or `pathlib.Path`) using `extract_file()`
|
117
134
|
- Extract text from a byte-string using `extract_bytes()`
|
118
135
|
|
119
|
-
Note - both of these functions are async and therefore should be used in an async context.
|
120
|
-
|
121
136
|
### Extract from File
|
122
137
|
|
123
138
|
```python
|
@@ -0,0 +1,13 @@
|
|
1
|
+
kreuzberg/__init__.py,sha256=5IBPjPsZ7faK15gFB9ZEROHhkEX7KKQmrHPCZuGnhb0,285
|
2
|
+
kreuzberg/_extractors.py,sha256=eZ12O7Ii2NRba-dDPIino_eKApCihfdxSPZP121D3xA,7541
|
3
|
+
kreuzberg/_mime_types.py,sha256=oJc4Qc2RkfZAYoCmxuuJ4S_Mo9-QQ0c4wwy0ZBqMRoA,2873
|
4
|
+
kreuzberg/_string.py,sha256=O023sxdYoC4DhFCU1z430UBdbxqwXKmyymUDDx3J_i8,1156
|
5
|
+
kreuzberg/_sync.py,sha256=ovsFHFdkcczz7gNEUJsbZzY8KHG0_GAOOYipQNE4hIY,874
|
6
|
+
kreuzberg/exceptions.py,sha256=jrXyvcuSU-694OEtXPZfHYcUbpoRZzNKw9Lo3wIZwL0,770
|
7
|
+
kreuzberg/extraction.py,sha256=j5p-JCnyGMouRp4qD-1qKSV_cw8DQ9QSo1H2ocwbbqA,5732
|
8
|
+
kreuzberg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
kreuzberg-1.2.0.dist-info/LICENSE,sha256=-8caMvpCK8SgZ5LlRKhGCMtYDEXqTKH9X8pFEhl91_4,1066
|
10
|
+
kreuzberg-1.2.0.dist-info/METADATA,sha256=rTuoCAAk9mYh7f55bLSQ9CfEPhh3BnqNHwxdruth1P8,8330
|
11
|
+
kreuzberg-1.2.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
12
|
+
kreuzberg-1.2.0.dist-info/top_level.txt,sha256=rbGkygffkZiyKhL8UN41ZOjLfem0jJPA1Whtndne0rE,10
|
13
|
+
kreuzberg-1.2.0.dist-info/RECORD,,
|
kreuzberg-1.1.0.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
kreuzberg/__init__.py,sha256=5IBPjPsZ7faK15gFB9ZEROHhkEX7KKQmrHPCZuGnhb0,285
|
2
|
-
kreuzberg/_extractors.py,sha256=r8L9Bm3x7s1u7-T5HKkr1j6M6W3bUuwMAmDtAwX-s9g,4717
|
3
|
-
kreuzberg/_mime_types.py,sha256=M5sKT4OkMf7pwtgs_jO2uhl6gC94wUurYzw_wbrIjU0,2739
|
4
|
-
kreuzberg/_string.py,sha256=5s6BfTLQdYlDEt2PP4AdmBLV-ajroATOVYQQRcBYFD4,934
|
5
|
-
kreuzberg/_sync.py,sha256=ovsFHFdkcczz7gNEUJsbZzY8KHG0_GAOOYipQNE4hIY,874
|
6
|
-
kreuzberg/exceptions.py,sha256=jrXyvcuSU-694OEtXPZfHYcUbpoRZzNKw9Lo3wIZwL0,770
|
7
|
-
kreuzberg/extraction.py,sha256=-a_msLQm7h5pHDhBuvfRP81-FtBwv7FGW-6YVJlXpUg,4926
|
8
|
-
kreuzberg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
-
kreuzberg-1.1.0.dist-info/LICENSE,sha256=-8caMvpCK8SgZ5LlRKhGCMtYDEXqTKH9X8pFEhl91_4,1066
|
10
|
-
kreuzberg-1.1.0.dist-info/METADATA,sha256=nkDjE2MEqAE_-1MZvlBxnNuM7SKCOD2LvB7Ucb_W7U4,7775
|
11
|
-
kreuzberg-1.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
12
|
-
kreuzberg-1.1.0.dist-info/top_level.txt,sha256=rbGkygffkZiyKhL8UN41ZOjLfem0jJPA1Whtndne0rE,10
|
13
|
-
kreuzberg-1.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|