HowdenParser 2.0.3__tar.gz → 3.1.0__tar.gz
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.
- {howdenparser-2.0.3 → howdenparser-3.1.0}/HowdenParser/parser.py +125 -46
- {howdenparser-2.0.3 → howdenparser-3.1.0}/PKG-INFO +1 -1
- {howdenparser-2.0.3 → howdenparser-3.1.0}/pyproject.toml +1 -1
- {howdenparser-2.0.3 → howdenparser-3.1.0}/HowdenParser/__init__.py +0 -0
- {howdenparser-2.0.3 → howdenparser-3.1.0}/HowdenParser/parameter/__init__.py +0 -0
- {howdenparser-2.0.3 → howdenparser-3.1.0}/HowdenParser/parameter/huggingface.py +0 -0
- {howdenparser-2.0.3 → howdenparser-3.1.0}/HowdenParser/parameter/llamaparser.py +0 -0
- {howdenparser-2.0.3 → howdenparser-3.1.0}/HowdenParser/parameter/mistralocr.py +0 -0
- {howdenparser-2.0.3 → howdenparser-3.1.0}/README.md +0 -0
|
@@ -4,8 +4,8 @@ import logging
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from PyPDF2 import PdfReader
|
|
6
6
|
import dotenv
|
|
7
|
-
import
|
|
8
|
-
from inspect import signature
|
|
7
|
+
from typing import overload, Literal
|
|
8
|
+
from inspect import signature, Signature
|
|
9
9
|
|
|
10
10
|
dotenv.load_dotenv()
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
@@ -14,6 +14,10 @@ logger = logging.getLogger(__name__)
|
|
|
14
14
|
class BaseParser(ABC):
|
|
15
15
|
_registry: dict[str, type["BaseParser"]] = {}
|
|
16
16
|
|
|
17
|
+
def __init__(self):
|
|
18
|
+
# Every instance gets a .name attribute
|
|
19
|
+
self.name = getattr(self.__class__, "_parser_name", self.__class__.__name__)
|
|
20
|
+
|
|
17
21
|
def __init_subclass__(cls, name: str | None = None, **kwargs):
|
|
18
22
|
"""Automatically register subclasses under a key."""
|
|
19
23
|
super().__init_subclass__(**kwargs)
|
|
@@ -29,6 +33,13 @@ class BaseParser(ABC):
|
|
|
29
33
|
class Parser(BaseParser):
|
|
30
34
|
"""Factory + registry interface for all parsers."""
|
|
31
35
|
|
|
36
|
+
def __new__(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
|
|
37
|
+
"""
|
|
38
|
+
Factory entrypoint.
|
|
39
|
+
Allows calling Parser(...) directly to create the correct subclass.
|
|
40
|
+
"""
|
|
41
|
+
return cls._create(config_or_dict, **kwargs)
|
|
42
|
+
|
|
32
43
|
@classmethod
|
|
33
44
|
def available_parsers(cls) -> None:
|
|
34
45
|
"""Print registered parsers and their init arguments."""
|
|
@@ -37,53 +48,130 @@ class Parser(BaseParser):
|
|
|
37
48
|
for name, parser_cls in BaseParser._registry.items():
|
|
38
49
|
sig = inspect.signature(parser_cls.__init__)
|
|
39
50
|
result[name] = [p for p in sig.parameters if p != "self"]
|
|
40
|
-
result.pop(
|
|
51
|
+
result.pop("", None)
|
|
41
52
|
for key, values in result.items():
|
|
42
53
|
print(f"{key} with parameters: {values}")
|
|
43
54
|
|
|
55
|
+
# ---- Overloads for IDE autocomplete ----
|
|
56
|
+
@overload
|
|
57
|
+
@classmethod
|
|
58
|
+
def _create(
|
|
59
|
+
cls,
|
|
60
|
+
*,
|
|
61
|
+
provider_and_model: Literal["llamaparser:"],
|
|
62
|
+
result_type: str,
|
|
63
|
+
mode: bool,
|
|
64
|
+
preserve_layout_alignment_across_pages: bool,
|
|
65
|
+
merge_tables_across_pages_in_markdown: bool
|
|
66
|
+
) -> "LlamaParser": ...
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
@classmethod
|
|
70
|
+
def _create(
|
|
71
|
+
cls,
|
|
72
|
+
*,
|
|
73
|
+
provider_and_model: Literal["mistralocr:ocr-large", "mistralocr:ocr-small"],
|
|
74
|
+
) -> "MistralOCRParser": ...
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
@classmethod
|
|
78
|
+
def _create(
|
|
79
|
+
cls,
|
|
80
|
+
*,
|
|
81
|
+
provider_and_model: Literal["langchain:gpt-3.5-turbo", "langchain:gpt-4"],
|
|
82
|
+
) -> "LangChainParser": ...
|
|
83
|
+
|
|
84
|
+
@overload
|
|
44
85
|
@classmethod
|
|
45
|
-
def
|
|
86
|
+
def _create(
|
|
87
|
+
cls,
|
|
88
|
+
*,
|
|
89
|
+
provider_and_model: Literal[
|
|
90
|
+
"huggingface:microsoft/trocr-base-handwritten",
|
|
91
|
+
"huggingface:microsoft/trocr-large-printed",
|
|
92
|
+
],
|
|
93
|
+
) -> "HuggingFaceParser": ...
|
|
94
|
+
|
|
95
|
+
# ---- Implementation ----
|
|
96
|
+
@classmethod
|
|
97
|
+
def _create(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
|
|
46
98
|
"""
|
|
47
|
-
Dynamically create parser instances
|
|
48
|
-
|
|
99
|
+
Dynamically create parser instances.
|
|
100
|
+
Supports: Parameter, dict, str (provider_and_model), or kwargs.
|
|
49
101
|
"""
|
|
50
|
-
|
|
51
|
-
if
|
|
102
|
+
|
|
103
|
+
if config_or_dict is None:
|
|
104
|
+
config_dict = {}
|
|
105
|
+
elif hasattr(config_or_dict, "model_dump"):
|
|
52
106
|
config_dict = config_or_dict.model_dump()
|
|
53
107
|
elif isinstance(config_or_dict, dict):
|
|
54
108
|
config_dict = config_or_dict
|
|
109
|
+
elif isinstance(config_or_dict, str): # shorthand
|
|
110
|
+
config_dict = {"provider_and_model": config_or_dict}
|
|
55
111
|
else:
|
|
56
|
-
raise TypeError("Expected Parameter instance or
|
|
57
|
-
|
|
58
|
-
if "provider_and_model" not in kwargs and "provider_and_model" not in config_dict:
|
|
59
|
-
raise ValueError("provider_and_model must be specified")
|
|
112
|
+
raise TypeError("Expected Parameter instance, dict, str, or None for config_or_dict")
|
|
60
113
|
|
|
61
|
-
# Merge dict + kwargs (kwargs take precedence)
|
|
62
114
|
merged_args = {**config_dict, **kwargs}
|
|
63
115
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
116
|
+
if "provider_and_model" not in merged_args:
|
|
117
|
+
raise ValueError("provider_and_model must be specified, e.g. 'llamaparser:'")
|
|
118
|
+
|
|
119
|
+
provider_and_model = str(merged_args.get("provider_and_model")).strip()
|
|
120
|
+
|
|
121
|
+
if ":" not in provider_and_model:
|
|
122
|
+
raise ValueError("provider_and_model must include a colon, e.g. 'llamaparser:'")
|
|
123
|
+
|
|
124
|
+
provider, model = provider_and_model.split(":", 1)
|
|
125
|
+
provider = (provider or "").strip().lower()
|
|
126
|
+
model = (model or "").strip().lower()
|
|
127
|
+
|
|
128
|
+
if not provider:
|
|
129
|
+
raise ValueError(f"Invalid provider_and_model '{provider_and_model}': provider part is empty.")
|
|
69
130
|
|
|
70
131
|
if provider not in BaseParser._registry:
|
|
71
132
|
raise ValueError(f"Unknown parser '{provider}'. Available: {list(BaseParser._registry)}")
|
|
72
133
|
|
|
73
134
|
parser_cls = BaseParser._registry[provider]
|
|
74
135
|
|
|
75
|
-
#
|
|
136
|
+
# --- Filter valid constructor args ---
|
|
76
137
|
sig = signature(parser_cls.__init__)
|
|
77
138
|
valid_args = {k: v for k, v in merged_args.items() if k in sig.parameters and k != "self"}
|
|
78
139
|
|
|
79
|
-
# Auto-
|
|
140
|
+
# Auto-fill common args
|
|
80
141
|
if "model" in sig.parameters and "model" not in valid_args:
|
|
81
142
|
valid_args["model"] = model
|
|
82
|
-
|
|
83
|
-
# Auto-insert 'provider_and_model' if required
|
|
84
143
|
if "provider_and_model" in sig.parameters and "provider_and_model" not in valid_args:
|
|
85
144
|
valid_args["provider_and_model"] = provider_and_model
|
|
86
145
|
|
|
146
|
+
# --- Check required args ---
|
|
147
|
+
required_params = [
|
|
148
|
+
p.name
|
|
149
|
+
for p in sig.parameters.values()
|
|
150
|
+
if p.name != "self"
|
|
151
|
+
and p.default is Signature.empty
|
|
152
|
+
and p.kind in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY)
|
|
153
|
+
]
|
|
154
|
+
missing = [name for name in required_params if name not in valid_args]
|
|
155
|
+
|
|
156
|
+
# Special rule: for llamaparser we allow empty `model`
|
|
157
|
+
if provider != "llamaparser" and not model:
|
|
158
|
+
missing.append("model")
|
|
159
|
+
|
|
160
|
+
if missing:
|
|
161
|
+
example = ""
|
|
162
|
+
if provider == "llamaparser":
|
|
163
|
+
example = "Example: Parser('llamaparser:', result_type='md', mode=True, extract_tables=True)"
|
|
164
|
+
elif provider == "mistralocr":
|
|
165
|
+
example = "Example: Parser('mistralocr:ocr-large')"
|
|
166
|
+
elif provider == "huggingface":
|
|
167
|
+
example = "Example: Parser('huggingface:microsoft/trocr-base-handwritten')"
|
|
168
|
+
elif provider == "langchain":
|
|
169
|
+
example = "Example: Parser('langchain:gpt-3.5-turbo')"
|
|
170
|
+
|
|
171
|
+
raise TypeError(
|
|
172
|
+
f"{parser_cls.__name__} cannot be created because it is missing required argument(s): {', '.join(missing)}. {example}"
|
|
173
|
+
)
|
|
174
|
+
|
|
87
175
|
return parser_cls(**valid_args)
|
|
88
176
|
|
|
89
177
|
@abstractmethod
|
|
@@ -93,24 +181,21 @@ class Parser(BaseParser):
|
|
|
93
181
|
|
|
94
182
|
# --- Parsers ---
|
|
95
183
|
class MistralOCRParser(BaseParser, name="mistralocr"):
|
|
96
|
-
def __init__(self,provider_and_model:str) -> None:
|
|
184
|
+
def __init__(self, provider_and_model: str) -> None:
|
|
97
185
|
from mistralai import Mistral
|
|
98
|
-
|
|
99
186
|
self.model = provider_and_model.split(":")[1]
|
|
100
187
|
self.current_cost: float = 0.0
|
|
101
188
|
self.total_cost_euro: float = 0.0
|
|
102
|
-
|
|
103
189
|
api_key = os.getenv("MISTRAL-OCR-API-TOKEN")
|
|
104
190
|
if not api_key:
|
|
105
191
|
raise EnvironmentError("Missing MISTRAL-OCR-API-TOKEN in .env file.")
|
|
106
|
-
|
|
107
192
|
self.client = Mistral(api_key=api_key)
|
|
108
193
|
|
|
109
194
|
def parse(self, file_path: Path) -> str:
|
|
110
195
|
def upload_pdf(filename):
|
|
111
196
|
uploaded_pdf = self.client.files.upload(
|
|
112
197
|
file={"file_name": filename, "content": open(filename, "rb")},
|
|
113
|
-
purpose="ocr"
|
|
198
|
+
purpose="ocr",
|
|
114
199
|
)
|
|
115
200
|
signed_url = self.client.files.get_signed_url(file_id=uploaded_pdf.id)
|
|
116
201
|
return signed_url.url
|
|
@@ -120,14 +205,11 @@ class MistralOCRParser(BaseParser, name="mistralocr"):
|
|
|
120
205
|
document={"type": "document_url", "document_url": upload_pdf(file_path)},
|
|
121
206
|
include_image_base64=True,
|
|
122
207
|
)
|
|
123
|
-
|
|
124
208
|
self.current_cost = 1 / 1000 * self._count_pages(file_path)
|
|
125
209
|
self.total_cost_euro += self.current_cost
|
|
126
|
-
|
|
127
210
|
return "\n".join(doc.markdown for doc in ocr_response.pages)
|
|
128
211
|
|
|
129
212
|
def __call__(self, file_path: Path) -> str:
|
|
130
|
-
# When instance is called, it delegates to parse
|
|
131
213
|
return self.parse(file_path)
|
|
132
214
|
|
|
133
215
|
@staticmethod
|
|
@@ -147,52 +229,52 @@ class LangChainParser(BaseParser, name="langchain"):
|
|
|
147
229
|
return {"source": "LangChain", "output": response}
|
|
148
230
|
|
|
149
231
|
def __call__(self, file_path: Path) -> str:
|
|
150
|
-
# When instance is called, it delegates to parse
|
|
151
232
|
return self.parse(file_path)
|
|
152
233
|
|
|
153
234
|
|
|
154
235
|
class LlamaParser(BaseParser, name="llamaparser"):
|
|
155
|
-
def __init__(self, result_type: str,
|
|
236
|
+
def __init__(self, result_type: str,
|
|
237
|
+
mode: bool,
|
|
238
|
+
provider_and_model: str,
|
|
239
|
+
merge_tables_across_pages_in_markdown: bool,
|
|
240
|
+
preserve_layout_alignment_across_pages: bool) -> None:
|
|
156
241
|
logging.info("Initializing LlamaParser...")
|
|
157
|
-
|
|
158
242
|
self.result_type = result_type
|
|
159
243
|
self.mode = mode
|
|
160
244
|
self.provider_and_model = provider_and_model
|
|
245
|
+
self.merge_tables_across_pages_in_markdown = merge_tables_across_pages_in_markdown
|
|
246
|
+
self.preserve_layout_alignment_across_pages = preserve_layout_alignment_across_pages
|
|
161
247
|
|
|
162
248
|
from llama_parse import LlamaParse, ResultType
|
|
163
|
-
|
|
164
249
|
if result_type.lower() in ("md", "markdown"):
|
|
165
250
|
result_type = ResultType.MD
|
|
166
|
-
|
|
167
251
|
api_key = os.getenv("LLAMA-PARSER-API-TOKEN")
|
|
168
252
|
if not api_key:
|
|
169
253
|
raise EnvironmentError("Missing LLAMA-PARSER-API-TOKEN in .env file.")
|
|
170
|
-
|
|
171
|
-
|
|
254
|
+
self._parser = LlamaParse(api_key=api_key,
|
|
255
|
+
result_type=result_type,
|
|
256
|
+
premium_mode=self.mode,
|
|
257
|
+
merge_tables_across_pages_in_markdown=self.merge_tables_across_pages_in_markdown,
|
|
258
|
+
preserve_layout_alignment_across_pages=self.preserve_layout_alignment_across_pages)
|
|
172
259
|
|
|
173
260
|
def parse(self, file_path: Path) -> str:
|
|
174
261
|
documents = self._parser.load_data(str(file_path))
|
|
175
262
|
return "\n".join(doc.text for doc in documents)
|
|
176
263
|
|
|
177
264
|
def __call__(self, file_path: Path) -> str:
|
|
178
|
-
# When instance is called, it delegates to parse
|
|
179
265
|
return self.parse(file_path)
|
|
180
266
|
|
|
181
267
|
|
|
182
268
|
class HuggingFaceParser(BaseParser, name="huggingface"):
|
|
183
269
|
def __init__(self, provider_and_model: str) -> None:
|
|
184
270
|
from transformers import TrOCRProcessor, VisionEncoderDecoderModel
|
|
185
|
-
|
|
186
271
|
api_key = os.getenv("HF-API-TOKEN")
|
|
187
272
|
if not api_key:
|
|
188
273
|
raise EnvironmentError("Missing HF-API-TOKEN in .env file.")
|
|
189
|
-
|
|
190
274
|
model_name = provider_and_model.split(":")[1]
|
|
191
275
|
logger.info(f"Loading Hugging Face OCR model: {model_name}")
|
|
192
|
-
|
|
193
276
|
self.processor = TrOCRProcessor.from_pretrained(model_name, token=api_key)
|
|
194
277
|
self.model = VisionEncoderDecoderModel.from_pretrained(model_name, token=api_key)
|
|
195
|
-
|
|
196
278
|
logger.info("Model and processor loaded successfully.")
|
|
197
279
|
|
|
198
280
|
def parse(self, file_path: Path) -> str:
|
|
@@ -208,9 +290,6 @@ class HuggingFaceParser(BaseParser, name="huggingface"):
|
|
|
208
290
|
generated_ids = self.model.generate(pixel_values)
|
|
209
291
|
text = self.processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
|
210
292
|
all_text += text + "\n"
|
|
211
|
-
logger.debug(f"OCR text (page {i}): {text[:100]}...")
|
|
212
|
-
|
|
293
|
+
logger.debug(f"OCR text (page {i}): {text[:100]}...")
|
|
213
294
|
logger.info("OCR completed for all pages.")
|
|
214
295
|
return all_text
|
|
215
|
-
|
|
216
|
-
|
|
@@ -14,7 +14,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
14
14
|
|
|
15
15
|
[tool.poetry]
|
|
16
16
|
name = "HowdenParser"
|
|
17
|
-
version = "
|
|
17
|
+
version = "3.1.0"
|
|
18
18
|
description = "A simple configuration manager with Pydantic and JSON export."
|
|
19
19
|
authors = [ "JesperThoftIllemannJ <jesper.jaeger@howdendanmark.dk>",]
|
|
20
20
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|