HowdenParser 4.0.4__tar.gz → 4.2.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.
@@ -0,0 +1,8 @@
1
+ from .parser import Parser
2
+
3
+ # Import all parsers to trigger registration (side effect imports)
4
+ from .parsers.llama_parser import LlamaParser # noqa: F401
5
+ from .parsers.mistral_parser import MistralOCRParser # noqa: F401
6
+ from .parsers.langchain_parser import LangChainParser # noqa: F401
7
+
8
+ __all__ = ["Parser"]
@@ -0,0 +1,2 @@
1
+ from .mistralocr import Parameter as MistralocrParameter
2
+ from .llamaparser import Parameter as LlamaparserParameter
@@ -0,0 +1,165 @@
1
+ from abc import ABC, abstractmethod
2
+ import logging
3
+ import dotenv
4
+ from typing import overload, Literal
5
+ from inspect import signature, Signature
6
+
7
+ dotenv.load_dotenv()
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class BaseParser(ABC):
12
+ _registry: dict[str, type["BaseParser"]] = {}
13
+
14
+ def __init__(self):
15
+ # Every instance gets a .name attribute
16
+ self.name = getattr(self.__class__, "_parser_name", self.__class__.__name__)
17
+
18
+ def __init_subclass__(cls, name: str | None = None, **kwargs):
19
+ """Automatically register subclasses under a key."""
20
+ super().__init_subclass__(**kwargs)
21
+ key = name or cls.__name__.lower().replace("parser", "")
22
+ BaseParser._registry[key] = cls
23
+ BaseParser._registry.pop("", None)
24
+
25
+ @abstractmethod
26
+ def parse(self, text: str, include_pagenumbers: bool = False):
27
+ pass
28
+
29
+
30
+ class Parser(BaseParser):
31
+ """Factory + registry interface for all parsers."""
32
+
33
+ def __new__(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
34
+ """
35
+ Factory entrypoint.
36
+ Allows calling Parser(...) directly to create the correct subclass.
37
+ """
38
+ return cls._create(config_or_dict, **kwargs)
39
+
40
+ @classmethod
41
+ def available_parsers(cls) -> None:
42
+ """Print registered parsers and their init arguments."""
43
+ import inspect
44
+ result = {}
45
+ for name, parser_cls in BaseParser._registry.items():
46
+ sig = inspect.signature(parser_cls.__init__)
47
+ result[name] = [p for p in sig.parameters if p != "self"]
48
+ result.pop("", None)
49
+ for key, values in result.items():
50
+ print(f"{key} with parameters: {values}")
51
+
52
+ # ---- Overloads for IDE autocomplete ----
53
+ @overload
54
+ @classmethod
55
+ def _create(
56
+ cls,
57
+ *,
58
+ provider_and_model: Literal["llamaparser:"],
59
+ result_type: str,
60
+ mode: bool,
61
+ preserve_layout_alignment_across_pages: bool,
62
+ merge_tables_across_pages_in_markdown: bool,
63
+ hide_footers: bool,
64
+ hide_headers: bool,
65
+ ) -> "LlamaParser": ...
66
+
67
+ @overload
68
+ @classmethod
69
+ def _create(
70
+ cls,
71
+ *,
72
+ provider_and_model: Literal["mistralocr:ocr-large", "mistralocr:ocr-small"],
73
+ ) -> "MistralOCRParser": ...
74
+
75
+ @overload
76
+ @classmethod
77
+ def _create(
78
+ cls,
79
+ *,
80
+ provider_and_model: Literal["langchain:gpt-3.5-turbo", "langchain:gpt-4"],
81
+ ) -> "LangChainParser": ...
82
+
83
+ # ---- Implementation ----
84
+ @classmethod
85
+ def _create(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
86
+ """
87
+ Dynamically create parser instances.
88
+ Supports: Parameter, dict, str (provider_and_model), or kwargs.
89
+ """
90
+
91
+ if config_or_dict is None:
92
+ config_dict = {}
93
+ elif hasattr(config_or_dict, "model_dump"):
94
+ config_dict = config_or_dict.model_dump()
95
+ elif isinstance(config_or_dict, dict):
96
+ config_dict = config_or_dict
97
+ elif isinstance(config_or_dict, str): # shorthand
98
+ config_dict = {"provider_and_model": config_or_dict}
99
+ else:
100
+ raise TypeError("Expected Parameter instance, dict, str, or None for config_or_dict")
101
+
102
+ merged_args = {**config_dict, **kwargs}
103
+
104
+ if "provider_and_model" not in merged_args:
105
+ raise ValueError("provider_and_model must be specified, e.g. 'llamaparser:'")
106
+
107
+ provider_and_model = str(merged_args.get("provider_and_model")).strip()
108
+
109
+ if ":" not in provider_and_model:
110
+ raise ValueError("provider_and_model must include a colon, e.g. 'llamaparser:'")
111
+
112
+ provider, model = provider_and_model.split(":", 1)
113
+ provider = (provider or "").strip().lower()
114
+ model = (model or "").strip().lower()
115
+
116
+ if not provider:
117
+ raise ValueError(f"Invalid provider_and_model '{provider_and_model}': provider part is empty.")
118
+
119
+ if provider not in BaseParser._registry:
120
+ raise ValueError(f"Unknown parser '{provider}'. Available: {list(BaseParser._registry)}")
121
+
122
+ parser_cls = BaseParser._registry[provider]
123
+
124
+ # --- Filter valid constructor args ---
125
+ sig = signature(parser_cls.__init__)
126
+ valid_args = {k: v for k, v in merged_args.items() if k in sig.parameters and k != "self"}
127
+
128
+ # Auto-fill common args
129
+ if "model" in sig.parameters and "model" not in valid_args:
130
+ valid_args["model"] = model
131
+ if "provider_and_model" in sig.parameters and "provider_and_model" not in valid_args:
132
+ valid_args["provider_and_model"] = provider_and_model
133
+
134
+ # --- Check required args ---
135
+ required_params = [
136
+ p.name
137
+ for p in sig.parameters.values()
138
+ if p.name != "self"
139
+ and p.default is Signature.empty
140
+ and p.kind in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY)
141
+ ]
142
+ missing = [name for name in required_params if name not in valid_args]
143
+
144
+ # Special rule: for llamaparser we allow empty `model`
145
+ if provider != "llamaparser" and not model:
146
+ missing.append("model")
147
+
148
+ if missing:
149
+ example = ""
150
+ if provider == "llamaparser":
151
+ example = "Example: Parser('llamaparser:', result_type='md', mode=True, extract_tables=True)"
152
+ elif provider == "mistralocr":
153
+ example = "Example: Parser('mistralocr:ocr-large')"
154
+ elif provider == "langchain":
155
+ example = "Example: Parser('langchain:gpt-3.5-turbo')"
156
+
157
+ raise TypeError(
158
+ f"{parser_cls.__name__} cannot be created because it is missing required argument(s): {', '.join(missing)}. {example}"
159
+ )
160
+
161
+ return parser_cls(**valid_args)
162
+
163
+ @abstractmethod
164
+ def parse(self, text: str, include_pagenumbers: bool = False):
165
+ pass
File without changes
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+ from ..parser import BaseParser
3
+
4
+
5
+
6
+ class LangChainParser(BaseParser, name="langchain"):
7
+ def __init__(self, provider_and_model: str):
8
+ from langchain.llms import OpenAI
9
+ self.model = provider_and_model.split(":")[1]
10
+ self.model = OpenAI(model_name=self.model)
11
+
12
+ def parse(self, text: str) -> dict:
13
+ response = self.model(text)
14
+ return {"source": "LangChain", "output": response}
15
+
16
+ def __call__(self, file_path: Path) -> str:
17
+ return self.parse(file_path)
18
+
@@ -0,0 +1,61 @@
1
+ import os
2
+ import logging
3
+ from pathlib import Path
4
+ from ..parser import BaseParser
5
+
6
+ class LlamaParser(BaseParser, name="llamaparser"):
7
+ def __init__(self, result_type: str,
8
+ mode: bool,
9
+ provider_and_model: str,
10
+ merge_tables_across_pages_in_markdown: bool,
11
+ preserve_layout_alignment_across_pages: bool,
12
+ hide_footers: bool,
13
+ hide_headers: bool
14
+ ) -> None:
15
+ logging.info("Initializing LlamaParser...")
16
+ self.result_type = result_type
17
+ self.mode = mode
18
+ self.provider_and_model = provider_and_model
19
+ self.merge_tables_across_pages_in_markdown = merge_tables_across_pages_in_markdown
20
+ self.preserve_layout_alignment_across_pages = preserve_layout_alignment_across_pages
21
+ self.hide_footers=hide_footers
22
+ self.hide_headers=hide_headers
23
+
24
+ from llama_parse import LlamaParse, ResultType
25
+ if result_type.lower() in ("md", "markdown"):
26
+ result_type = ResultType.MD
27
+ api_key = os.getenv("LLAMA-PARSER-API-TOKEN")
28
+ if not api_key:
29
+ raise EnvironmentError("Missing LLAMA-PARSER-API-TOKEN in .env file.")
30
+ self._parser = LlamaParse(api_key=api_key,
31
+ result_type=result_type,
32
+ premium_mode=self.mode,
33
+ merge_tables_across_pages_in_markdown=self.merge_tables_across_pages_in_markdown,
34
+ preserve_layout_alignment_across_pages=self.preserve_layout_alignment_across_pages,
35
+ hide_footers=self.hide_footers,
36
+ hide_headers=self.hide_headers)
37
+
38
+ def parse(self, file_path: Path, include_pagenumbers: bool=False) -> str:
39
+ """
40
+ Parse the document at file_path.
41
+ Args:
42
+ file_path (Path): Path to the document to be parsed.
43
+ include_pagenumbers (bool): Whether to include page numbers in the output. If True, page numbers will be wrapped around each page's content, like so: <PAGE_NUMBER 1>...content...</PAGE_NUMBER 1>
44
+ """
45
+
46
+ if not include_pagenumbers:
47
+ documents = self._parser.load_data(str(file_path))
48
+
49
+ result = "\n".join(doc.text for doc in documents)
50
+ else:
51
+ documents = self._parser.parse(file_path)
52
+
53
+ if self.result_type.lower() in ("md", "markdown"):
54
+ result = "\n".join(f"<PAGE_NUMBER {idx}>{page.md}</PAGE_NUMBER {idx}>" for idx, page in enumerate(documents.pages, start=1))
55
+ else:
56
+ result = "\n".join(f"<PAGE_NUMBER {idx}>{page.text}</PAGE_NUMBER {idx}>" for idx, page in enumerate(documents.pages, start=1))
57
+
58
+ return result
59
+
60
+ def __call__(self, file_path: Path) -> str:
61
+ return self.parse(file_path)
@@ -0,0 +1,43 @@
1
+ import os
2
+ from pathlib import Path
3
+ from ..parser import BaseParser
4
+ from PyPDF2 import PdfReader
5
+
6
+
7
+ class MistralOCRParser(BaseParser, name="mistralocr"):
8
+ def __init__(self, provider_and_model: str) -> None:
9
+ from mistralai import Mistral
10
+ self.model = provider_and_model.split(":")[1]
11
+ self.current_cost: float = 0.0
12
+ self.total_cost_euro: float = 0.0
13
+ api_key = os.getenv("MISTRAL-OCR-API-TOKEN")
14
+ if not api_key:
15
+ raise EnvironmentError("Missing MISTRAL-OCR-API-TOKEN in .env file.")
16
+ self.client = Mistral(api_key=api_key)
17
+
18
+ def parse(self, file_path: Path) -> str:
19
+ def upload_pdf(filename):
20
+ uploaded_pdf = self.client.files.upload(
21
+ file={"file_name": filename, "content": open(filename, "rb")},
22
+ purpose="ocr",
23
+ )
24
+ signed_url = self.client.files.get_signed_url(file_id=uploaded_pdf.id)
25
+ return signed_url.url
26
+
27
+ ocr_response = self.client.ocr.process(
28
+ model=self.model,
29
+ document={"type": "document_url", "document_url": upload_pdf(file_path)},
30
+ include_image_base64=True,
31
+ )
32
+ self.current_cost = 1 / 1000 * self._count_pages(file_path)
33
+ self.total_cost_euro += self.current_cost
34
+ return "\n".join(doc.markdown for doc in ocr_response.pages)
35
+
36
+ def __call__(self, file_path: Path) -> str:
37
+ return self.parse(file_path)
38
+
39
+ @staticmethod
40
+ def _count_pages(file_path: Path) -> int:
41
+ reader = PdfReader(str(file_path))
42
+ return len(reader.pages)
43
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HowdenParser
3
- Version: 4.0.4
3
+ Version: 4.2.0
4
4
  Summary: A simple configuration manager with Pydantic and JSON export.
5
5
  License: MIT
6
6
  Keywords: config,configuration,pydantic,json
@@ -11,15 +11,11 @@ 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
- Provides-Extra: dev
15
14
  Requires-Dist: langchain (>=0.3.27,<0.4.0)
16
15
  Requires-Dist: llama-parse (>=0.6.58,<0.7.0)
17
16
  Requires-Dist: mistralai (>=1.9.3,<2.0.0)
18
17
  Requires-Dist: pdf2image (>=1.17.0,<2.0.0)
19
18
  Requires-Dist: pypdf2 (>=3.0.1,<4.0.0)
20
- Requires-Dist: toml (>=0.10.2,<0.11.0) ; extra == "dev"
21
- Requires-Dist: tomli-w (>=1.2.0,<2.0.0) ; extra == "dev"
22
- Requires-Dist: transformers (==4.56.0)
23
19
  Description-Content-Type: text/markdown
24
20
 
25
21
  # OCR & LLM Parser
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "HowdenParser"
3
- version = "4.0.4"
3
+ version = "4.2.0"
4
4
  description = "A simple configuration manager with Pydantic and JSON export."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.14"
@@ -22,18 +22,11 @@ dependencies = [
22
22
  "langchain (>=0.3.27,<0.4.0)",
23
23
  "pypdf2 (>=3.0.1,<4.0.0)",
24
24
  "pdf2image (>=1.17.0,<2.0.0)",
25
- "transformers (==4.56.0)",
26
25
  ]
27
26
 
28
27
  [project.license]
29
28
  text = "MIT"
30
29
 
31
- [project.optional-dependencies]
32
- dev = [
33
- "toml (>=0.10.2,<0.11.0)",
34
- "tomli-w (>=1.2.0,<2.0.0)",
35
- ]
36
-
37
30
  [build-system]
38
31
  requires = [
39
32
  "poetry-core>=2.0.0,<3.0.0",
@@ -42,5 +35,7 @@ build-backend = "poetry.core.masonry.api"
42
35
 
43
36
  [dependency-groups]
44
37
  dev = [
38
+ "toml (>=0.10.2,<0.11.0)",
45
39
  "tomli-w (>=1.2.0,<2.0.0)",
40
+ "howdenconfig (>=1.0.6,<2.0.0)",
46
41
  ]
@@ -1,3 +0,0 @@
1
- from .parser import Parser
2
-
3
- __all__ = ["Parser"]
@@ -1,3 +0,0 @@
1
- from .huggingface import Parameter as HuggingfaceParameter
2
- from .mistralocr import Parameter as MistralocrParameter
3
- from .llamaparser import Parameter as LlamaparserParameter
@@ -1,6 +0,0 @@
1
- from typing import Literal
2
- from pydantic import BaseModel
3
-
4
- class Parameter(BaseModel):
5
- model: str = "huggingface:HURIDOCS/pdf-segmentation"
6
-
@@ -1,304 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- import os
3
- import logging
4
- from pathlib import Path
5
- from PyPDF2 import PdfReader
6
- import dotenv
7
- from typing import overload, Literal
8
- from inspect import signature, Signature
9
-
10
- dotenv.load_dotenv()
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class BaseParser(ABC):
15
- _registry: dict[str, type["BaseParser"]] = {}
16
-
17
- def __init__(self):
18
- # Every instance gets a .name attribute
19
- self.name = getattr(self.__class__, "_parser_name", self.__class__.__name__)
20
-
21
- def __init_subclass__(cls, name: str | None = None, **kwargs):
22
- """Automatically register subclasses under a key."""
23
- super().__init_subclass__(**kwargs)
24
- key = name or cls.__name__.lower().replace("parser", "")
25
- BaseParser._registry[key] = cls
26
- BaseParser._registry.pop("", None)
27
-
28
- @abstractmethod
29
- def parse(self, text: str):
30
- pass
31
-
32
-
33
- class Parser(BaseParser):
34
- """Factory + registry interface for all parsers."""
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
-
43
- @classmethod
44
- def available_parsers(cls) -> None:
45
- """Print registered parsers and their init arguments."""
46
- import inspect
47
- result = {}
48
- for name, parser_cls in BaseParser._registry.items():
49
- sig = inspect.signature(parser_cls.__init__)
50
- result[name] = [p for p in sig.parameters if p != "self"]
51
- result.pop("", None)
52
- for key, values in result.items():
53
- print(f"{key} with parameters: {values}")
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
- hide_footers: bool,
67
- hide_headers: bool,
68
- ) -> "LlamaParser": ...
69
-
70
- @overload
71
- @classmethod
72
- def _create(
73
- cls,
74
- *,
75
- provider_and_model: Literal["mistralocr:ocr-large", "mistralocr:ocr-small"],
76
- ) -> "MistralOCRParser": ...
77
-
78
- @overload
79
- @classmethod
80
- def _create(
81
- cls,
82
- *,
83
- provider_and_model: Literal["langchain:gpt-3.5-turbo", "langchain:gpt-4"],
84
- ) -> "LangChainParser": ...
85
-
86
- @overload
87
- @classmethod
88
- def _create(
89
- cls,
90
- *,
91
- provider_and_model: Literal[
92
- "huggingface:microsoft/trocr-base-handwritten",
93
- "huggingface:microsoft/trocr-large-printed",
94
- ],
95
- ) -> "HuggingFaceParser": ...
96
-
97
- # ---- Implementation ----
98
- @classmethod
99
- def _create(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
100
- """
101
- Dynamically create parser instances.
102
- Supports: Parameter, dict, str (provider_and_model), or kwargs.
103
- """
104
-
105
- if config_or_dict is None:
106
- config_dict = {}
107
- elif hasattr(config_or_dict, "model_dump"):
108
- config_dict = config_or_dict.model_dump()
109
- elif isinstance(config_or_dict, dict):
110
- config_dict = config_or_dict
111
- elif isinstance(config_or_dict, str): # shorthand
112
- config_dict = {"provider_and_model": config_or_dict}
113
- else:
114
- raise TypeError("Expected Parameter instance, dict, str, or None for config_or_dict")
115
-
116
- merged_args = {**config_dict, **kwargs}
117
-
118
- if "provider_and_model" not in merged_args:
119
- raise ValueError("provider_and_model must be specified, e.g. 'llamaparser:'")
120
-
121
- provider_and_model = str(merged_args.get("provider_and_model")).strip()
122
-
123
- if ":" not in provider_and_model:
124
- raise ValueError("provider_and_model must include a colon, e.g. 'llamaparser:'")
125
-
126
- provider, model = provider_and_model.split(":", 1)
127
- provider = (provider or "").strip().lower()
128
- model = (model or "").strip().lower()
129
-
130
- if not provider:
131
- raise ValueError(f"Invalid provider_and_model '{provider_and_model}': provider part is empty.")
132
-
133
- if provider not in BaseParser._registry:
134
- raise ValueError(f"Unknown parser '{provider}'. Available: {list(BaseParser._registry)}")
135
-
136
- parser_cls = BaseParser._registry[provider]
137
-
138
- # --- Filter valid constructor args ---
139
- sig = signature(parser_cls.__init__)
140
- valid_args = {k: v for k, v in merged_args.items() if k in sig.parameters and k != "self"}
141
-
142
- # Auto-fill common args
143
- if "model" in sig.parameters and "model" not in valid_args:
144
- valid_args["model"] = model
145
- if "provider_and_model" in sig.parameters and "provider_and_model" not in valid_args:
146
- valid_args["provider_and_model"] = provider_and_model
147
-
148
- # --- Check required args ---
149
- required_params = [
150
- p.name
151
- for p in sig.parameters.values()
152
- if p.name != "self"
153
- and p.default is Signature.empty
154
- and p.kind in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY)
155
- ]
156
- missing = [name for name in required_params if name not in valid_args]
157
-
158
- # Special rule: for llamaparser we allow empty `model`
159
- if provider != "llamaparser" and not model:
160
- missing.append("model")
161
-
162
- if missing:
163
- example = ""
164
- if provider == "llamaparser":
165
- example = "Example: Parser('llamaparser:', result_type='md', mode=True, extract_tables=True)"
166
- elif provider == "mistralocr":
167
- example = "Example: Parser('mistralocr:ocr-large')"
168
- elif provider == "huggingface":
169
- example = "Example: Parser('huggingface:microsoft/trocr-base-handwritten')"
170
- elif provider == "langchain":
171
- example = "Example: Parser('langchain:gpt-3.5-turbo')"
172
-
173
- raise TypeError(
174
- f"{parser_cls.__name__} cannot be created because it is missing required argument(s): {', '.join(missing)}. {example}"
175
- )
176
-
177
- return parser_cls(**valid_args)
178
-
179
- @abstractmethod
180
- def parse(self, text: str):
181
- pass
182
-
183
-
184
- # --- Parsers ---
185
- class MistralOCRParser(BaseParser, name="mistralocr"):
186
- def __init__(self, provider_and_model: str) -> None:
187
- from mistralai import Mistral
188
- self.model = provider_and_model.split(":")[1]
189
- self.current_cost: float = 0.0
190
- self.total_cost_euro: float = 0.0
191
- api_key = os.getenv("MISTRAL-OCR-API-TOKEN")
192
- if not api_key:
193
- raise EnvironmentError("Missing MISTRAL-OCR-API-TOKEN in .env file.")
194
- self.client = Mistral(api_key=api_key)
195
-
196
- def parse(self, file_path: Path) -> str:
197
- def upload_pdf(filename):
198
- uploaded_pdf = self.client.files.upload(
199
- file={"file_name": filename, "content": open(filename, "rb")},
200
- purpose="ocr",
201
- )
202
- signed_url = self.client.files.get_signed_url(file_id=uploaded_pdf.id)
203
- return signed_url.url
204
-
205
- ocr_response = self.client.ocr.process(
206
- model=self.model,
207
- document={"type": "document_url", "document_url": upload_pdf(file_path)},
208
- include_image_base64=True,
209
- )
210
- self.current_cost = 1 / 1000 * self._count_pages(file_path)
211
- self.total_cost_euro += self.current_cost
212
- return "\n".join(doc.markdown for doc in ocr_response.pages)
213
-
214
- def __call__(self, file_path: Path) -> str:
215
- return self.parse(file_path)
216
-
217
- @staticmethod
218
- def _count_pages(file_path: Path) -> int:
219
- reader = PdfReader(str(file_path))
220
- return len(reader.pages)
221
-
222
-
223
- class LangChainParser(BaseParser, name="langchain"):
224
- def __init__(self, provider_and_model: str):
225
- from langchain.llms import OpenAI
226
- self.model = provider_and_model.split(":")[1]
227
- self.model = OpenAI(model_name=self.model)
228
-
229
- def parse(self, text: str) -> dict:
230
- response = self.model(text)
231
- return {"source": "LangChain", "output": response}
232
-
233
- def __call__(self, file_path: Path) -> str:
234
- return self.parse(file_path)
235
-
236
-
237
- class LlamaParser(BaseParser, name="llamaparser"):
238
- def __init__(self, result_type: str,
239
- mode: bool,
240
- provider_and_model: str,
241
- merge_tables_across_pages_in_markdown: bool,
242
- preserve_layout_alignment_across_pages: bool,
243
- hide_footers: bool,
244
- hide_headers: bool
245
- ) -> None:
246
- logging.info("Initializing LlamaParser...")
247
- self.result_type = result_type
248
- self.mode = mode
249
- self.provider_and_model = provider_and_model
250
- self.merge_tables_across_pages_in_markdown = merge_tables_across_pages_in_markdown
251
- self.preserve_layout_alignment_across_pages = preserve_layout_alignment_across_pages
252
- self.hide_footers=hide_footers
253
- self.hide_headers=hide_headers
254
-
255
- from llama_parse import LlamaParse, ResultType
256
- if result_type.lower() in ("md", "markdown"):
257
- result_type = ResultType.MD
258
- api_key = os.getenv("LLAMA-PARSER-API-TOKEN")
259
- if not api_key:
260
- raise EnvironmentError("Missing LLAMA-PARSER-API-TOKEN in .env file.")
261
- self._parser = LlamaParse(api_key=api_key,
262
- result_type=result_type,
263
- premium_mode=self.mode,
264
- merge_tables_across_pages_in_markdown=self.merge_tables_across_pages_in_markdown,
265
- preserve_layout_alignment_across_pages=self.preserve_layout_alignment_across_pages,
266
- hide_footers=self.hide_footers,
267
- hide_headers=self.hide_headers)
268
-
269
- def parse(self, file_path: Path) -> str:
270
- documents = self._parser.load_data(str(file_path))
271
- return "\n".join(doc.text for doc in documents)
272
-
273
- def __call__(self, file_path: Path) -> str:
274
- return self.parse(file_path)
275
-
276
-
277
- class HuggingFaceParser(BaseParser, name="huggingface"):
278
- def __init__(self, provider_and_model: str) -> None:
279
- from transformers import TrOCRProcessor, VisionEncoderDecoderModel
280
- api_key = os.getenv("HF-API-TOKEN")
281
- if not api_key:
282
- raise EnvironmentError("Missing HF-API-TOKEN in .env file.")
283
- model_name = provider_and_model.split(":")[1]
284
- logger.info(f"Loading Hugging Face OCR model: {model_name}")
285
- self.processor = TrOCRProcessor.from_pretrained(model_name, token=api_key)
286
- self.model = VisionEncoderDecoderModel.from_pretrained(model_name, token=api_key)
287
- logger.info("Model and processor loaded successfully.")
288
-
289
- def parse(self, file_path: Path) -> str:
290
- from pdf2image import convert_from_path
291
- logger.info(f"Converting PDF to images: {file_path}")
292
- pages = convert_from_path(file_path, dpi=300)
293
- logger.info(f"PDF conversion complete. Total pages: {len(pages)}")
294
-
295
- all_text = ""
296
- for i, page in enumerate(pages, start=1):
297
- logger.info(f"Running OCR on page {i}/{len(pages)}")
298
- pixel_values = self.processor(page, return_tensors="pt").pixel_values
299
- generated_ids = self.model.generate(pixel_values)
300
- text = self.processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
301
- all_text += text + "\n"
302
- logger.debug(f"OCR text (page {i}): {text[:100]}...")
303
- logger.info("OCR completed for all pages.")
304
- return all_text
File without changes