HowdenParser 1.0.0__tar.gz → 2.0.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.
@@ -3,6 +3,6 @@ from typing import Literal
3
3
  from pydantic import BaseModel
4
4
 
5
5
  class Parameter(BaseModel):
6
- model: str = "llamaparser:"
6
+ provider_and_model: str = "llamaparser:"
7
7
  result_type: Literal["md"] = "md"
8
8
  mode: bool = False
@@ -4,8 +4,11 @@ import logging
4
4
  from pathlib import Path
5
5
  from PyPDF2 import PdfReader
6
6
  import dotenv
7
+ import logging
8
+ from inspect import signature
7
9
 
8
10
  dotenv.load_dotenv()
11
+ logger = logging.getLogger(__name__)
9
12
 
10
13
 
11
14
  class BaseParser(ABC):
@@ -27,38 +30,61 @@ class Parser(BaseParser):
27
30
  """Factory + registry interface for all parsers."""
28
31
 
29
32
  @classmethod
30
- def available_parsers(cls) -> dict[str, list[str]]:
31
- """Return registered parsers and their init arguments."""
33
+ def available_parsers(cls) -> None:
34
+ """Print registered parsers and their init arguments."""
32
35
  import inspect
33
36
  result = {}
34
37
  for name, parser_cls in BaseParser._registry.items():
35
38
  sig = inspect.signature(parser_cls.__init__)
36
39
  result[name] = [p for p in sig.parameters if p != "self"]
37
40
  result.pop('', None)
38
- return result
41
+ for key, values in result.items():
42
+ print(f"{key} with parameters: {values}")
39
43
 
40
44
  @classmethod
41
- def create(cls, config: dict | None = None, **kwargs) -> BaseParser:
42
- provider = kwargs["provider_and_model"].split(":")[0].lower()
43
- model = kwargs["provider_and_model"].split(":")[1].lower()
45
+ def create(cls, config_or_dict: "Parameter | dict", **kwargs) -> BaseParser:
46
+ """
47
+ Dynamically create parser instances from a config object or dict.
48
+ Only passes arguments accepted by the parser constructor.
49
+ """
50
+ # Convert Parameter -> dict if needed
51
+ if hasattr(config_or_dict, "model_dump"):
52
+ config_dict = config_or_dict.model_dump()
53
+ elif isinstance(config_or_dict, dict):
54
+ config_dict = config_or_dict
55
+ else:
56
+ raise TypeError("Expected Parameter instance or dict for config_or_dict")
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")
60
+
61
+ # Merge dict + kwargs (kwargs take precedence)
62
+ merged_args = {**config_dict, **kwargs}
63
+
64
+ # Extract provider/model info
65
+ provider_and_model = merged_args.get("provider_and_model")
66
+ provider, model = provider_and_model.split(":")
67
+ provider = provider.lower()
68
+ model = model.lower()
69
+
44
70
  if provider not in BaseParser._registry:
45
- raise ValueError(f"Unknown parser '{provider}'. "
46
- f"Available: {cls.available_parsers()}")
71
+ raise ValueError(f"Unknown parser '{provider}'. Available: {list(BaseParser._registry)}")
47
72
 
48
73
  parser_cls = BaseParser._registry[provider]
49
- import inspect
50
- merged_args = {**(config or {}), **kwargs}
51
- if "model" in inspect.signature(parser_cls.__init__).parameters:
52
- merged_args["model"] = model
53
74
 
54
- # Remove keys not in constructor
55
- sig = inspect.signature(parser_cls.__init__)
75
+ # Inspect constructor and only pass valid arguments
76
+ sig = signature(parser_cls.__init__)
56
77
  valid_args = {k: v for k, v in merged_args.items() if k in sig.parameters and k != "self"}
57
78
 
58
- return parser_cls(**valid_args)
59
-
79
+ # Auto-insert 'model' if required
80
+ if "model" in sig.parameters and "model" not in valid_args:
81
+ valid_args["model"] = model
60
82
 
83
+ # Auto-insert 'provider_and_model' if required
84
+ if "provider_and_model" in sig.parameters and "provider_and_model" not in valid_args:
85
+ valid_args["provider_and_model"] = provider_and_model
61
86
 
87
+ return parser_cls(**valid_args)
62
88
 
63
89
  @abstractmethod
64
90
  def parse(self, text: str):
@@ -107,10 +133,10 @@ class MistralOCRParser(BaseParser, name="mistralocr"):
107
133
 
108
134
 
109
135
  class LangChainParser(BaseParser, name="langchain"):
110
- def __init__(self, model: str):
136
+ def __init__(self, provider_and_model: str):
111
137
  from langchain.llms import OpenAI
112
- self.model_name = model
113
- self.model = OpenAI(model_name=model)
138
+ self.model = provider_and_model.split(":")[1]
139
+ self.model = OpenAI(model_name=self.model)
114
140
 
115
141
  def parse(self, text: str) -> dict:
116
142
  response = self.model(text)
@@ -138,36 +164,37 @@ class LlamaParser(BaseParser, name="llamaparser"):
138
164
 
139
165
 
140
166
  class HuggingFaceParser(BaseParser, name="huggingface"):
141
- def __init__(self, model: str, result_type: str) -> None:
142
- from transformers import pipeline
143
-
144
- if result_type.lower() in ("md", "markdown"):
145
- self.result_type = "markdown"
146
- else:
147
- self.result_type = "text"
167
+ def __init__(self, provider_and_model: str) -> None:
168
+ from transformers import TrOCRProcessor, VisionEncoderDecoderModel
148
169
 
149
170
  api_key = os.getenv("HF-API-TOKEN")
150
171
  if not api_key:
151
172
  raise EnvironmentError("Missing HF-API-TOKEN in .env file.")
152
173
 
153
- self.parser = pipeline(
154
- task="document-question-answering",
155
- model=model,
156
- use_auth_token=api_key
157
- )
174
+ model_name = provider_and_model.split(":")[1]
175
+ logger.info(f"Loading Hugging Face OCR model: {model_name}")
176
+
177
+ self.processor = TrOCRProcessor.from_pretrained(model_name, token=api_key)
178
+ self.model = VisionEncoderDecoderModel.from_pretrained(model_name, token=api_key)
179
+
180
+ logger.info("Model and processor loaded successfully.")
158
181
 
159
182
  def parse(self, file_path: Path) -> str:
160
- import fitz
161
- pdf_doc = fitz.open(file_path)
162
- output_parts = []
163
-
164
- for page in pdf_doc:
165
- pix = page.get_pixmap(dpi=200)
166
- img_bytes = pix.tobytes("png")
167
- response = self.parser(img_bytes, question="Extract all text")
168
- if response and "answer" in response[0]:
169
- output_parts.append(response[0]["answer"])
170
-
171
- return "\n\n".join(output_parts) if self.result_type == "markdown" else " ".join(output_parts)
183
+ from pdf2image import convert_from_path
184
+ logger.info(f"Converting PDF to images: {file_path}")
185
+ pages = convert_from_path(file_path, dpi=300)
186
+ logger.info(f"PDF conversion complete. Total pages: {len(pages)}")
187
+
188
+ all_text = ""
189
+ for i, page in enumerate(pages, start=1):
190
+ logger.info(f"Running OCR on page {i}/{len(pages)}")
191
+ pixel_values = self.processor(page, return_tensors="pt").pixel_values
192
+ generated_ids = self.model.generate(pixel_values)
193
+ text = self.processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
194
+ all_text += text + "\n"
195
+ logger.debug(f"OCR text (page {i}): {text[:100]}...") # preview first 100 chars
196
+
197
+ logger.info("OCR completed for all pages.")
198
+ return all_text
172
199
 
173
200
 
@@ -1,22 +1,27 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: HowdenParser
3
- Version: 1.0.0
3
+ Version: 2.0.0
4
4
  Summary: A simple configuration manager with Pydantic and JSON export.
5
5
  License: MIT
6
6
  Keywords: config,configuration,pydantic,json
7
7
  Author: JesperThoftIllemannJ
8
8
  Author-email: jesper.jaeger@howdendanmark.dk
9
- Requires-Python: >=3.12,<4.0
9
+ Requires-Python: >=3.12,<3.14
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: fitz (>=0.0.1.dev2,<0.0.2)
15
+ Requires-Dist: hf-xet (>=1.1.7,<2.0.0)
15
16
  Requires-Dist: howdenconfig (>=0.1.13,<0.2.0)
16
17
  Requires-Dist: langchain (>=0.3.27,<0.4.0)
17
18
  Requires-Dist: llama-parse (>=0.6.58,<0.7.0)
18
19
  Requires-Dist: mistralai (>=1.9.3,<2.0.0)
20
+ Requires-Dist: pdf2image (>=1.17.0,<2.0.0)
19
21
  Requires-Dist: pypdf2 (>=3.0.1,<4.0.0)
22
+ Requires-Dist: torch (>=2.8.0,<3.0.0)
23
+ Requires-Dist: torchaudio (>=2.8.0,<3.0.0)
24
+ Requires-Dist: torchvision (>=0.23.0,<0.24.0)
20
25
  Requires-Dist: transformers (>=4.55.2,<5.0.0)
21
26
  Project-URL: Documentation, https://github.com/yourusername/config
22
27
  Project-URL: Homepage, https://github.com/yourusername/config
@@ -73,8 +78,9 @@ print(text)
73
78
 
74
79
  if HowdenConfig package being used
75
80
 
81
+ config: Config = Config(parameter=Parameter())
76
82
 
77
- parser = ParserFactory.get_parser("mistralocr:", **config.parameter.dump_model())
83
+ parser = Parser.create(config.parameter)
78
84
 
79
85
  text = parser.parse(Path("document.pdf"))
80
86
 
@@ -48,8 +48,9 @@ print(text)
48
48
 
49
49
  if HowdenConfig package being used
50
50
 
51
+ config: Config = Config(parameter=Parameter())
51
52
 
52
- parser = ParserFactory.get_parser("mistralocr:", **config.parameter.dump_model())
53
+ parser = Parser.create(config.parameter)
53
54
 
54
55
  text = parser.parse(Path("document.pdf"))
55
56
 
@@ -2,8 +2,8 @@
2
2
  name = "HowdenParser"
3
3
  description = ""
4
4
  readme = "README.md"
5
- requires-python = ">=3.12,<4.0"
6
- dependencies = [ "mistralai (>=1.9.3,<2.0.0)", "llama-parse (>=0.6.58,<0.7.0)", "langchain (>=0.3.27,<0.4.0)", "transformers (>=4.55.2,<5.0.0)", "fitz (>=0.0.1.dev2,<0.0.2)", "howdenconfig (>=0.1.13,<0.2.0)", "pypdf2 (>=3.0.1,<4.0.0)",]
5
+ requires-python = ">=3.12,<3.14"
6
+ dependencies = [ "mistralai (>=1.9.3,<2.0.0)", "llama-parse (>=0.6.58,<0.7.0)", "langchain (>=0.3.27,<0.4.0)", "transformers (>=4.55.2,<5.0.0)", "fitz (>=0.0.1.dev2,<0.0.2)", "howdenconfig (>=0.1.13,<0.2.0)", "pypdf2 (>=3.0.1,<4.0.0)", "pdf2image (>=1.17.0,<2.0.0)", "torch (>=2.8.0,<3.0.0)", "torchvision (>=0.23.0,<0.24.0)", "torchaudio (>=2.8.0,<3.0.0)", "hf-xet (>=1.1.7,<2.0.0)",]
7
7
  [[project.authors]]
8
8
  name = "JesperThoftIllemannJ"
9
9
  email = "jesper.jaeger@howdendanmark.dk"
@@ -14,7 +14,7 @@ build-backend = "poetry.core.masonry.api"
14
14
 
15
15
  [tool.poetry]
16
16
  name = "HowdenParser"
17
- version = "1.0.0"
17
+ version = "2.0.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"
@@ -26,5 +26,14 @@ documentation = "https://github.com/yourusername/config"
26
26
  [[tool.poetry.packages]]
27
27
  include = "HowdenParser"
28
28
 
29
+ [tool.poetry.dependencies.torch]
30
+ source = "pypi"
31
+
32
+ [tool.poetry.dependencies.torchvision]
33
+ source = "pypi"
34
+
35
+ [tool.poetry.dependencies.torchaudio]
36
+ source = "pypi"
37
+
29
38
  [tool.poetry.group.dev.dependencies]
30
39
  toml = "^0.10.2"