ocr-llm 1.0.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.
llm_ocr/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """llm-ocr: Convert PDFs to markdown using LLMs."""
2
+
3
+ from .config.settings import OCRConfig
4
+ from .core.base import LLMProvider
5
+ from .core.processor import LLMOCR
6
+
7
+ # Import providers
8
+ from .providers.gemini import Gemini
9
+ from .providers.openai import OpenAI
10
+
11
+ __version__ = "1.0.0"
12
+ __all__ = [
13
+ "LLMOCR",
14
+ "OCRConfig",
15
+ "LLMProvider",
16
+ "Gemini",
17
+ "OpenAI",
18
+ ]
File without changes
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class OCRConfig:
7
+ """Configuration for OCR processing."""
8
+
9
+ dpi: int = 200
10
+ max_pages: Optional[int] = None
11
+ batch_size: int = 5 # PDF to image conversion batch size
12
+ llm_batch_size: int = 1 # Number of pages to send to LLM at once
13
+ thread_count: int = 4
14
+ convert_to_grayscale: bool = False
15
+ optimize_png: bool = True
16
+ use_cropbox: bool = True
17
+ max_retries: int = 3
18
+ retry_delay: float = 1.0
19
+ output_format: str = "markdown"
20
+ include_page_markers: bool = False
21
+
22
+ def validate(self):
23
+ """Validate configuration."""
24
+ if self.dpi < 72 or self.dpi > 600:
25
+ raise ValueError("DPI must be between 72 and 600")
26
+ if self.batch_size < 1:
27
+ raise ValueError("Batch size must be at least 1")
28
+ if self.llm_batch_size < 1:
29
+ raise ValueError("LLM batch size must be at least 1")
File without changes
llm_ocr/core/base.py ADDED
@@ -0,0 +1,48 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List
3
+
4
+
5
+ class LLMProvider(ABC):
6
+ """Abstract base class for LLM providers."""
7
+
8
+ def __init__(self, api_key: str, model: str, **kwargs):
9
+ """
10
+ Initialize the LLM provider.
11
+
12
+ Args:
13
+ api_key: API key for the provider
14
+ model: Model identifier/name
15
+ **kwargs: Additional provider-specific parameters
16
+ """
17
+ self.api_key = api_key
18
+ self.model = model
19
+ self.kwargs = kwargs
20
+
21
+ @abstractmethod
22
+ async def generate_content(
23
+ self,
24
+ prompt: str,
25
+ image_bytes_list: List[bytes],
26
+ ) -> str:
27
+ """
28
+ Generate content from images and prompt.
29
+
30
+ Args:
31
+ prompt: The instruction prompt
32
+ image_bytes_list: List of PNG image bytes (one or more pages)
33
+
34
+ Returns:
35
+ Generated text content
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ async def aclose(self):
41
+ """Close and cleanup resources."""
42
+ pass
43
+
44
+ @property
45
+ @abstractmethod
46
+ def name(self) -> str:
47
+ """Return the provider name."""
48
+ pass
@@ -0,0 +1,224 @@
1
+ import asyncio
2
+ import io
3
+ import tempfile
4
+ from pathlib import Path
5
+ from typing import AsyncIterable, List, Optional, Tuple, Union
6
+
7
+ from pdf2image import convert_from_path, pdfinfo_from_path
8
+ from PIL import Image
9
+
10
+ from ..config.settings import OCRConfig
11
+ from ..core.base import LLMProvider
12
+ from ..prompts.markdown import MARKDOWN_SYSTEM_PROMPT
13
+
14
+
15
+ class LLMOCR:
16
+ """Main processor for converting PDFs to markdown using LLMs."""
17
+
18
+ def __init__(self, provider: LLMProvider, config: Optional[OCRConfig] = None):
19
+ """
20
+ Initialize LLM OCR converter.
21
+
22
+ Args:
23
+ provider: An instance of an LLM provider (Gemini, OpenAI, etc.)
24
+ config: OCRConfig object for processing settings
25
+ """
26
+ if not isinstance(provider, LLMProvider):
27
+ raise TypeError(
28
+ f"provider must be an instance of LLMProvider, got {type(provider)}"
29
+ )
30
+
31
+ self.provider = provider
32
+ self.config = config or OCRConfig()
33
+ self.config.validate()
34
+
35
+ def _process_image_to_bytes(self, img_path: str) -> bytes:
36
+ """Convert image file to PNG bytes."""
37
+ with Image.open(img_path) as im:
38
+ if self.config.convert_to_grayscale:
39
+ im = im.convert("L")
40
+ buff = io.BytesIO()
41
+ im.save(buff, format="PNG", optimize=self.config.optimize_png)
42
+ return buff.getvalue()
43
+
44
+ async def _iter_pdf_pages_as_png_bytes(
45
+ self, pdf_path: Path
46
+ ) -> AsyncIterable[Tuple[int, bytes]]:
47
+ """Yield (page_number, png_bytes) for each page."""
48
+ info = pdfinfo_from_path(pdf_path)
49
+ total_pages = info["Pages"]
50
+ if self.config.max_pages:
51
+ total_pages = min(total_pages, self.config.max_pages)
52
+
53
+ page = 1
54
+ with tempfile.TemporaryDirectory() as tmpdir:
55
+ while page <= total_pages:
56
+ first = page
57
+ last = min(page + self.config.batch_size - 1, total_pages)
58
+
59
+ img_paths = convert_from_path(
60
+ pdf_path,
61
+ dpi=self.config.dpi,
62
+ first_page=first,
63
+ last_page=last,
64
+ output_folder=tmpdir,
65
+ fmt="png",
66
+ paths_only=True,
67
+ thread_count=self.config.thread_count,
68
+ use_cropbox=self.config.use_cropbox,
69
+ )
70
+
71
+ for p, img_path in enumerate(img_paths, start=first):
72
+ png_bytes = await asyncio.to_thread(
73
+ self._process_image_to_bytes, img_path
74
+ )
75
+ yield p, png_bytes
76
+
77
+ page = last + 1
78
+
79
+ async def _process_batch_with_llm(
80
+ self, png_bytes_list: List[bytes], page_nums: List[int], total_pages: int
81
+ ) -> str:
82
+ """Process a batch of pages with the LLM provider."""
83
+ # Format page range for prompt
84
+ if len(page_nums) == 1:
85
+ page_range = f"{page_nums[0]}"
86
+ else:
87
+ page_range = f"{page_nums[0]}-{page_nums[-1]}"
88
+
89
+ prompt = MARKDOWN_SYSTEM_PROMPT.format(
90
+ page_range=page_range, total_pages=total_pages
91
+ )
92
+
93
+ try:
94
+ content = await self.provider.generate_content(prompt, png_bytes_list)
95
+ return content.strip()
96
+ except Exception as err:
97
+ print(f"[ERROR] Pages {page_range}: Failed to process: {err}")
98
+ raise # Fail-fast: re-raise to stop processing
99
+
100
+ def _combine_markdown_chunks(self, chunks: List[str]) -> str:
101
+ """Combine markdown chunks from multiple pages."""
102
+ combined = []
103
+
104
+ for i, chunk in enumerate(chunks):
105
+ if not chunk.strip():
106
+ continue
107
+
108
+ # Add page separator comment for debugging (optional)
109
+ if i > 0 and self.config.include_page_markers:
110
+ combined.append(f"\n<!-- Page {i + 1} -->\n")
111
+
112
+ combined.append(chunk)
113
+
114
+ return "\n\n".join(combined)
115
+
116
+ async def aclose(self):
117
+ """Close and cleanup resources."""
118
+ await self.provider.aclose()
119
+
120
+ async def __aenter__(self):
121
+ """Async context manager entry."""
122
+ return self
123
+
124
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
125
+ """Async context manager exit."""
126
+ await self.aclose()
127
+
128
+ async def convert(
129
+ self, pdf_path: Union[str, Path], output_path: Optional[Union[str, Path]] = None
130
+ ) -> str:
131
+ """
132
+ Convert PDF to markdown.
133
+
134
+ Args:
135
+ pdf_path: Path to the PDF file
136
+ output_path: Optional path to save the markdown file
137
+
138
+ Returns:
139
+ Markdown content as string
140
+ """
141
+ pdf_path = Path(pdf_path)
142
+
143
+ if not pdf_path.exists():
144
+ raise FileNotFoundError(f"PDF file not found: {pdf_path}")
145
+
146
+ # Get total pages
147
+ info = pdfinfo_from_path(pdf_path)
148
+ total_pages = info["Pages"]
149
+ if self.config.max_pages:
150
+ total_pages = min(total_pages, self.config.max_pages)
151
+
152
+ print(f"[INFO] Converting PDF: {pdf_path.name} ({total_pages} pages)")
153
+ print(f"[INFO] Using provider: {self.provider.name} ({self.provider.model})")
154
+ print(f"[INFO] LLM batch size: {self.config.llm_batch_size}")
155
+
156
+ markdown_chunks = []
157
+
158
+ # Collect pages into batches according to llm_batch_size
159
+ page_batch = []
160
+ page_num_batch = []
161
+
162
+ async for page_num, png_bytes in self._iter_pdf_pages_as_png_bytes(pdf_path):
163
+ page_batch.append(png_bytes)
164
+ page_num_batch.append(page_num)
165
+
166
+ # Process batch when it reaches llm_batch_size or this is the last page
167
+ if len(page_batch) >= self.config.llm_batch_size or page_num == total_pages:
168
+ # Retry logic for transient errors
169
+ markdown_content = ""
170
+ for attempt in range(self.config.max_retries):
171
+ try:
172
+ markdown_content = await self._process_batch_with_llm(
173
+ page_batch, page_num_batch, total_pages
174
+ )
175
+ break
176
+ except Exception as e:
177
+ if attempt == self.config.max_retries - 1:
178
+ page_range = (
179
+ f"{page_num_batch[0]}-{page_num_batch[-1]}"
180
+ if len(page_num_batch) > 1
181
+ else str(page_num_batch[0])
182
+ )
183
+ print(
184
+ f"[ERROR] Pages {page_range}: Failed after "
185
+ f"{self.config.max_retries} attempts: {e}"
186
+ )
187
+ raise # Fail-fast: stop processing on failure
188
+ await asyncio.sleep(self.config.retry_delay * (attempt + 1))
189
+
190
+ if markdown_content.strip():
191
+ page_range = (
192
+ f"{page_num_batch[0]}-{page_num_batch[-1]}"
193
+ if len(page_num_batch) > 1
194
+ else str(page_num_batch[0])
195
+ )
196
+ print(
197
+ f"[OK] Pages {page_range}/{total_pages} processed "
198
+ f"({len(markdown_content)} chars)"
199
+ )
200
+ markdown_chunks.append(markdown_content)
201
+ else:
202
+ page_range = (
203
+ f"{page_num_batch[0]}-{page_num_batch[-1]}"
204
+ if len(page_num_batch) > 1
205
+ else str(page_num_batch[0])
206
+ )
207
+ print(f"[OK] Pages {page_range}/{total_pages} - No content")
208
+
209
+ # Clear the batch
210
+ page_batch = []
211
+ page_num_batch = []
212
+
213
+ # Combine all chunks
214
+ final_markdown = self._combine_markdown_chunks(markdown_chunks)
215
+
216
+ # Save to file if output path provided
217
+ if output_path:
218
+ output_path = Path(output_path)
219
+ output_path.parent.mkdir(parents=True, exist_ok=True)
220
+ output_path.write_text(final_markdown, encoding="utf-8")
221
+ print(f"[DONE] Saved to: {output_path}")
222
+
223
+ print(f"[DONE] Generated {len(final_markdown)} characters of markdown")
224
+ return final_markdown
File without changes
@@ -0,0 +1,33 @@
1
+ from textwrap import dedent
2
+
3
+ MARKDOWN_SYSTEM_PROMPT = dedent(
4
+ """
5
+ You are an expert at converting document images to markdown format.
6
+
7
+ Your task is to accurately transcribe all text content from the image(s) and format it as clean, readable markdown.
8
+
9
+ FORMATTING RULES:
10
+ - Use proper markdown headers (# ## ###) for document structure
11
+ - Preserve lists (ordered and unordered)
12
+ - Format tables using markdown table syntax
13
+ - Use **bold** and *italic* for emphasized text
14
+ - Preserve code blocks with ```language syntax
15
+ - Use > for blockquotes
16
+ - Keep links and images in markdown format
17
+ - Maintain paragraph breaks and spacing
18
+
19
+ CRITICAL RULES:
20
+ - DO NOT add any commentary, explanations, or meta-text
21
+ - DO NOT include markdown code fences (```) around your output
22
+ - Output ONLY the markdown content itself
23
+ - If a page is blank or contains no meaningful content, skip it
24
+ - Preserve the original document's structure and hierarchy
25
+ - For multi-column layouts, process left to right, top to bottom
26
+ - When processing multiple pages, combine them into a single coherent output
27
+ - Maintain continuity across pages (e.g., tables, paragraphs spanning pages)
28
+
29
+ PAGES: {page_range} of {total_pages}
30
+
31
+ Output only the markdown content:
32
+ """
33
+ )
@@ -0,0 +1,4 @@
1
+ from .gemini import Gemini
2
+ from .openai import OpenAI
3
+
4
+ __all__ = ["Gemini", "OpenAI"]
@@ -0,0 +1,87 @@
1
+ import os
2
+ from typing import List, Optional
3
+
4
+ from google.genai import Client, types
5
+
6
+ from ..core.base import LLMProvider
7
+
8
+
9
+ class Gemini(LLMProvider):
10
+ """
11
+ Google Gemini provider for OCR.
12
+
13
+ Supported models:
14
+ - gemini-2.5-pro (recommended, best quality)
15
+ - gemini-2.5-flash (fast)
16
+ - gemini-2.0-pro
17
+ - gemini-2.0-flash
18
+ """
19
+
20
+ # Available models with their identifiers
21
+ FLASH_2_5 = "gemini-2.5-flash"
22
+ PRO_2_5 = "gemini-2.5-pro"
23
+ FLASH_2_0 = "gemini-2.0-flash"
24
+ PRO_2_0 = "gemini-2.0-pro"
25
+
26
+ def __init__(self, api_key: Optional[str] = None, model: str = FLASH_2_5, **kwargs):
27
+ """
28
+ Initialize Gemini provider.
29
+
30
+ Args:
31
+ api_key: Gemini API key. If None, reads from GEMINI_API_KEY env var
32
+ model: Model to use. Use Gemini.FLASH_2_5, Gemini.PRO_2_5, etc.
33
+ **kwargs: Additional parameters for the Gemini API
34
+ """
35
+ api_key = api_key or os.getenv("GEMINI_API_KEY")
36
+ if not api_key:
37
+ raise ValueError(
38
+ "Gemini API key is required. "
39
+ "Pass it directly or set GEMINI_API_KEY environment variable."
40
+ )
41
+
42
+ super().__init__(api_key, model, **kwargs)
43
+
44
+ # Initialize the async client
45
+ client = Client(api_key=self.api_key)
46
+ self.aclient = client.aio
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ """Return provider name."""
51
+ return "gemini"
52
+
53
+ async def generate_content(
54
+ self,
55
+ prompt: str,
56
+ image_bytes_list: List[bytes],
57
+ ) -> str:
58
+ """Generate content using Gemini with one or more images."""
59
+ # Create image parts for all images in the batch
60
+ image_parts = [
61
+ types.Part(inline_data=types.Blob(mime_type="image/png", data=image_bytes))
62
+ for image_bytes in image_bytes_list
63
+ ]
64
+
65
+ # Combine prompt and all images
66
+ contents = [prompt] + image_parts
67
+
68
+ try:
69
+ resp = await self.aclient.models.generate_content(
70
+ model=self.model, contents=contents, **self.kwargs
71
+ )
72
+
73
+ try:
74
+ return resp.text or ""
75
+ except (ValueError, AttributeError):
76
+ print("[WARNING] Gemini returned no content")
77
+ return ""
78
+
79
+ except Exception as err:
80
+ print(f"[ERROR] Gemini generation failed: {err}")
81
+ raise
82
+
83
+ async def aclose(self):
84
+ """Close the Gemini client."""
85
+ # The Google GenAI AsyncClient doesn't require explicit closing
86
+ # Resources are cleaned up automatically
87
+ pass
@@ -0,0 +1,123 @@
1
+ import base64
2
+ import os
3
+ from typing import List, Optional
4
+
5
+ from openai import AsyncOpenAI
6
+
7
+ from ..core.base import LLMProvider
8
+
9
+
10
+ class OpenAI(LLMProvider):
11
+ """
12
+ OpenAI provider for OCR.
13
+
14
+ Supported models:
15
+ - gpt-5
16
+ - gpt-5-mini
17
+ - gpt-5-nano
18
+ - gpt-5-chat-latest
19
+ - gpt-4.1
20
+ - gpt-4.1-mini
21
+ - gpt-4.1-nano
22
+ - o4-mini
23
+ - o1
24
+ - o3
25
+ - gpt-4o
26
+ - gpt-4o-mini
27
+ """
28
+
29
+ # Available models with their identifiers
30
+ GPT_5 = "gpt-5"
31
+ GPT_5_MINI = "gpt-5-mini"
32
+ GPT_5_NANO = "gpt-5-nano"
33
+ GPT_5_CHAT_LATEST = "gpt-5-chat-latest"
34
+
35
+ GPT_4_1 = "gpt-4.1"
36
+ GPT_4_1_MINI = "gpt-4.1-mini"
37
+ GPT_4_1_NANO = "gpt-4.1-nano"
38
+
39
+ O4_MINI = "o4-mini"
40
+ O1 = "o1"
41
+ O3 = "o3"
42
+
43
+ GPT_4O = "gpt-4o"
44
+ GPT_4O_MINI = "gpt-4o-mini"
45
+
46
+ def __init__(
47
+ self, api_key: Optional[str] = None, model: str = GPT_4O_MINI, **kwargs
48
+ ):
49
+ """
50
+ Initialize OpenAI provider.
51
+
52
+ Args:
53
+ api_key: OpenAI API key. If None, reads from OPENAI_API_KEY env var
54
+ model: Model to use. Use OpenAI.GPT_4O, OpenAI.GPT_4O_MINI, etc.
55
+ **kwargs: Additional parameters for the OpenAI API (e.g., max_tokens, temperature)
56
+ """
57
+ api_key = api_key or os.getenv("OPENAI_API_KEY")
58
+ if not api_key:
59
+ raise ValueError(
60
+ "OpenAI API key is required. "
61
+ "Pass it directly or set OPENAI_API_KEY environment variable."
62
+ )
63
+
64
+ super().__init__(api_key, model, **kwargs)
65
+
66
+ # Initialize the async client
67
+ self.aclient = AsyncOpenAI(api_key=self.api_key)
68
+
69
+ @property
70
+ def name(self) -> str:
71
+ """Return provider name."""
72
+ return "openai"
73
+
74
+ async def generate_content(
75
+ self,
76
+ prompt: str,
77
+ image_bytes_list: List[bytes],
78
+ ) -> str:
79
+ """Generate content using OpenAI with one or more images."""
80
+ # Create content array with text prompt
81
+ content = [{"type": "text", "text": prompt}]
82
+
83
+ # Add all images as base64-encoded data URLs
84
+ for image_bytes in image_bytes_list:
85
+ b64_image = base64.b64encode(image_bytes).decode("utf-8")
86
+ content.append(
87
+ {
88
+ "type": "image_url",
89
+ "image_url": {
90
+ "url": f"data:image/png;base64,{b64_image}",
91
+ },
92
+ }
93
+ )
94
+
95
+ try:
96
+ # Use the Chat Completions API with vision
97
+ response = await self.aclient.chat.completions.create(
98
+ model=self.model,
99
+ messages=[
100
+ {
101
+ "role": "user",
102
+ "content": content,
103
+ }
104
+ ],
105
+ **self.kwargs,
106
+ )
107
+
108
+ # Extract the text content from the response
109
+ if response.choices and len(response.choices) > 0:
110
+ message_content = response.choices[0].message.content
111
+ return message_content or ""
112
+ else:
113
+ print("[WARNING] OpenAI returned no content")
114
+ return ""
115
+
116
+ except Exception as err:
117
+ print(f"[ERROR] OpenAI generation failed: {err}")
118
+ raise
119
+
120
+ async def aclose(self):
121
+ """Close the OpenAI client."""
122
+ # Close the async client
123
+ await self.aclient.close()
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: ocr-llm
3
+ Version: 1.0.0
4
+ Summary: Convert PDFs to markdown using Large Language Models (LLMs) with vision capabilities
5
+ Author-email: Shehryar Sohail <hafizshehryar88@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Shehryar718/llm-ocr
8
+ Project-URL: Repository, https://github.com/Shehryar718/llm-ocr
9
+ Project-URL: Issues, https://github.com/Shehryar718/llm-ocr/issues
10
+ Keywords: ocr,pdf,markdown,llm,vision,gemini,openai,ai
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
20
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: pdf2image>=1.16.0
25
+ Requires-Dist: Pillow>=11.0.0
26
+ Requires-Dist: google-genai>=1.0.0
27
+ Requires-Dist: openai>=2.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=1.0.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=6.5.0; extra == "dev"
32
+ Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
33
+ Requires-Dist: black>=24.0.0; extra == "dev"
34
+ Requires-Dist: isort>=6.0.0; extra == "dev"
35
+ Requires-Dist: flake8>=7.0.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.10.0; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # LLM OCR
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/ocr-llm)](https://pypi.org/project/ocr-llm/)
43
+ [![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue)](https://pypi.org/project/ocr-llm/)
44
+ [![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Shehryar718/llm-ocr/blob/main/LICENSE)
45
+
46
+ Convert PDFs to markdown using Large Language Models (LLMs) with vision capabilities.
47
+
48
+ ## Features
49
+
50
+ - 🔍 High-quality OCR using vision-capable LLMs
51
+ - 📄 Batch processing of multiple PDF pages
52
+ - 🔌 Multiple provider support (Gemini, OpenAI)
53
+ - ⚙️ Configurable processing settings
54
+ - 🔄 Automatic retry logic for transient errors
55
+ - 📝 Clean markdown output
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install ocr-llm
61
+ ```
62
+
63
+ ### System Dependencies
64
+
65
+ You also need to install poppler (required for PDF processing):
66
+
67
+ ```bash
68
+ # macOS
69
+ brew install poppler
70
+
71
+ # Ubuntu/Debian
72
+ sudo apt-get install poppler-utils
73
+
74
+ # Fedora/RHEL
75
+ sudo yum install poppler-utils
76
+ ```
77
+
78
+ ### Dependencies
79
+
80
+ The library requires:
81
+
82
+ - **System**: `poppler-utils` for PDF processing
83
+ - **Python**:
84
+ - `google-genai` for Gemini provider
85
+ - `openai` for OpenAI provider
86
+ - `pdf2image` and `Pillow` for PDF processing
87
+
88
+ ## Quick Start
89
+
90
+ ### Using OpenAI
91
+
92
+ ```python
93
+ import asyncio
94
+ from llm_ocr import LLMOCR, OpenAI
95
+
96
+ async def main():
97
+ # Initialize OpenAI provider
98
+ provider = OpenAI(
99
+ api_key="your-api-key", # Or set OPENAI_API_KEY env var
100
+ model=OpenAI.GPT_4O_MINI
101
+ )
102
+
103
+ # Create OCR processor
104
+ async with LLMOCR(provider) as ocr:
105
+ # Convert PDF to markdown
106
+ markdown = await ocr.convert(
107
+ "document.pdf",
108
+ output_path="output.md"
109
+ )
110
+ print(markdown)
111
+
112
+ asyncio.run(main())
113
+ ```
114
+
115
+ ### Using Gemini
116
+
117
+ ```python
118
+ import asyncio
119
+ from llm_ocr import LLMOCR, Gemini
120
+
121
+ async def main():
122
+ # Initialize Gemini provider
123
+ provider = Gemini(
124
+ api_key="your-api-key", # Or set GEMINI_API_KEY env var
125
+ model=Gemini.FLASH_2_5 # Or Gemini.PRO_2_5 for best quality
126
+ )
127
+
128
+ # Create OCR processor
129
+ async with LLMOCR(provider) as ocr:
130
+ # Convert PDF to markdown
131
+ markdown = await ocr.convert(
132
+ "document.pdf",
133
+ output_path="output.md"
134
+ )
135
+ print(markdown)
136
+
137
+ asyncio.run(main())
138
+ ```
139
+
140
+ ## Available Models
141
+
142
+ ### OpenAI
143
+
144
+ - `OpenAI.GPT_4O`
145
+ - `OpenAI.GPT_4O_MINI` (default)
146
+
147
+ Additional models: `O1`, `O3`, `O4_MINI`, `GPT_5`, `GPT_5_MINI`, `GPT_4_1`, and more.
148
+
149
+ > See `llm_ocr/providers/openai.py` for the complete list.
150
+
151
+ ### Gemini
152
+
153
+ - `Gemini.PRO_2_5`
154
+ - `Gemini.FLASH_2_5` (default)
155
+
156
+ Additional models: `PRO_2_0`, `FLASH_2_0`.
157
+
158
+ > See `llm_ocr/providers/gemini.py` for the complete list.
159
+
160
+ ## Configuration
161
+
162
+ Customize the OCR processing with `OCRConfig`:
163
+
164
+ ```python
165
+ from llm_ocr import LLMOCR, OpenAI, OCRConfig
166
+
167
+ config = OCRConfig(
168
+ dpi=300, # Higher DPI for better quality
169
+ max_pages=10, # Limit number of pages to process
170
+ llm_batch_size=2, # Send 2 pages to LLM at once
171
+ convert_to_grayscale=True, # Convert images to grayscale
172
+ max_retries=3, # Retry failed requests
173
+ retry_delay=1.0, # Wait 1 second between retries
174
+ include_page_markers=True, # Add page markers in output
175
+ )
176
+
177
+ provider = OpenAI()
178
+ ocr = LLMOCR(provider, config=config)
179
+ ```
180
+
181
+ ### Configuration Options
182
+
183
+ | Option | Default | Description |
184
+ | ---------------------- | ------- | ------------------------------------------ |
185
+ | `dpi` | 200 | DPI for PDF to image conversion (72-600) |
186
+ | `max_pages` | None | Maximum number of pages to process |
187
+ | `batch_size` | 5 | PDF to image conversion batch size |
188
+ | `llm_batch_size` | 1 | Number of pages to send to LLM at once |
189
+ | `thread_count` | 4 | Number of threads for PDF conversion |
190
+ | `convert_to_grayscale` | False | Convert images to grayscale |
191
+ | `optimize_png` | True | Optimize PNG compression |
192
+ | `use_cropbox` | True | Use PDF cropbox for conversion |
193
+ | `max_retries` | 3 | Maximum retry attempts for failed requests |
194
+ | `retry_delay` | 1.0 | Delay between retries in seconds |
195
+ | `include_page_markers` | False | Add page markers in markdown output |
196
+
197
+ ## Advanced Usage
198
+
199
+ ### Custom Provider Parameters
200
+
201
+ Pass additional parameters to the LLM provider:
202
+
203
+ ```python
204
+ # OpenAI with custom parameters
205
+ provider = OpenAI(
206
+ model=OpenAI.GPT_4O,
207
+ max_tokens=4000,
208
+ temperature=0.0,
209
+ )
210
+
211
+ # Gemini with custom parameters
212
+ provider = Gemini(
213
+ model=Gemini.PRO_2_5,
214
+ temperature=0.0,
215
+ )
216
+ ```
217
+
218
+ ### Processing Multiple Documents
219
+
220
+ ```python
221
+ import asyncio
222
+ from pathlib import Path
223
+ from llm_ocr import LLMOCR, OpenAI
224
+
225
+ async def process_documents():
226
+ provider = OpenAI()
227
+
228
+ async with LLMOCR(provider) as ocr:
229
+ pdf_files = Path("pdfs").glob("*.pdf")
230
+
231
+ for pdf_file in pdf_files:
232
+ output_file = pdf_file.with_suffix(".md")
233
+ await ocr.convert(pdf_file, output_path=output_file)
234
+ print(f"Converted {pdf_file.name} -> {output_file.name}")
235
+
236
+ asyncio.run(process_documents())
237
+ ```
238
+
239
+ ### Without Context Manager
240
+
241
+ If you prefer not to use the context manager:
242
+
243
+ ```python
244
+ import asyncio
245
+ from llm_ocr import LLMOCR, OpenAI
246
+
247
+ async def main():
248
+ provider = OpenAI()
249
+ ocr = LLMOCR(provider)
250
+
251
+ try:
252
+ markdown = await ocr.convert("document.pdf")
253
+ print(markdown)
254
+ finally:
255
+ await ocr.aclose() # Don't forget to close!
256
+
257
+ asyncio.run(main())
258
+ ```
259
+
260
+ ## Environment Variables
261
+
262
+ Set API keys via environment variables:
263
+
264
+ ```bash
265
+ # For OpenAI
266
+ export OPENAI_API_KEY="your-openai-api-key"
267
+
268
+ # For Gemini
269
+ export GEMINI_API_KEY="your-gemini-api-key"
270
+ ```
271
+
272
+ Then use providers without passing API keys:
273
+
274
+ ```python
275
+ # API key read from environment variable
276
+ provider = OpenAI() # Uses OPENAI_API_KEY
277
+ # or
278
+ provider = Gemini() # Uses GEMINI_API_KEY
279
+ ```
280
+
281
+ ## Error Handling
282
+
283
+ The library uses a fail-fast approach with automatic retries:
284
+
285
+ ```python
286
+ import asyncio
287
+ from llm_ocr import LLMOCR, OpenAI, OCRConfig
288
+
289
+ async def main():
290
+ provider = OpenAI()
291
+ config = OCRConfig(
292
+ max_retries=5, # Retry up to 5 times
293
+ retry_delay=2.0, # Wait 2 seconds between retries
294
+ )
295
+
296
+ async with LLMOCR(provider, config) as ocr:
297
+ try:
298
+ markdown = await ocr.convert("document.pdf")
299
+ print(markdown)
300
+ except Exception as e:
301
+ print(f"Failed to process document: {e}")
302
+
303
+ asyncio.run(main())
304
+ ```
305
+
306
+ ## License
307
+
308
+ See LICENSE file for details.
@@ -0,0 +1,16 @@
1
+ llm_ocr/__init__.py,sha256=85oykW2FSKOFMZKUccUW5wEs8Wlk5-8Q8KBPyL0GF4c,371
2
+ llm_ocr/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ llm_ocr/config/settings.py,sha256=WzZfNKTj1VZ4yS0bCEmVX9R5IxwGtMeje9x6hiHzE9U,943
4
+ llm_ocr/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ llm_ocr/core/base.py,sha256=guS3dTsGBFzAKXTjIni3w2oeHNM6EAOQYqaKLoOkJqw,1140
6
+ llm_ocr/core/processor.py,sha256=kbCnmXNaRoyVyA2m170FARKf_Z2nbZDgcRLZPMj2Xr0,8380
7
+ llm_ocr/prompts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ llm_ocr/prompts/markdown.py,sha256=ik6QhFLVsnxDVTN-z1S7tjHaZGcZJ8wcdnCPhfv5ZHY,1336
9
+ llm_ocr/providers/__init__.py,sha256=Sf8t5ZxwOZ7XlwxX0OSXlgYMPhAaEc6JhUdO58zgk6k,86
10
+ llm_ocr/providers/gemini.py,sha256=_Fo-AKF5XStQ-7stti2GfBbs9DQsf88hUo0vcJxfhKw,2603
11
+ llm_ocr/providers/openai.py,sha256=RFuwsaYae3WDQd6V1plN4q88owg9B4Q2W0h1GwdfHvw,3402
12
+ ocr_llm-1.0.0.dist-info/licenses/LICENSE,sha256=YNyaca-Sr7AUZJKoh_1wWi9GN6PRNZErWfIASRtCBAM,1072
13
+ ocr_llm-1.0.0.dist-info/METADATA,sha256=5j_lVKX0Ef_KaGoR5dD5sF9uzbWaKV3001ADLUy67Sc,8411
14
+ ocr_llm-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ ocr_llm-1.0.0.dist-info/top_level.txt,sha256=kUf9ARPXFNWvv-LRnhJPYz7BbBgPNDi9uesrPduvD0k,8
16
+ ocr_llm-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shehryar Sohail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ llm_ocr