mistocr 0.0.4__tar.gz → 0.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.
- {mistocr-0.0.4/mistocr.egg-info → mistocr-0.1.0}/PKG-INFO +2 -1
- mistocr-0.1.0/mistocr/__init__.py +1 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr/_modidx.py +8 -1
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr/core.py +14 -17
- mistocr-0.1.0/mistocr/refine.py +113 -0
- {mistocr-0.0.4 → mistocr-0.1.0/mistocr.egg-info}/PKG-INFO +2 -1
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr.egg-info/SOURCES.txt +1 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr.egg-info/requires.txt +1 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/settings.ini +2 -2
- mistocr-0.0.4/mistocr/__init__.py +0 -1
- {mistocr-0.0.4 → mistocr-0.1.0}/LICENSE +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/MANIFEST.in +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/README.md +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr.egg-info/dependency_links.txt +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr.egg-info/entry_points.txt +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr.egg-info/not-zip-safe +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/mistocr.egg-info/top_level.txt +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/pyproject.toml +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/setup.cfg +0 -0
- {mistocr-0.0.4 → mistocr-0.1.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mistocr
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Simple batch OCR for PDFs using Mistral's state-of-the-art vision model
|
|
5
5
|
Home-page: https://github.com/franckalbinet/mistocr
|
|
6
6
|
Author: Solveit
|
|
@@ -22,6 +22,7 @@ Requires-Dist: fastcore
|
|
|
22
22
|
Requires-Dist: mistralai
|
|
23
23
|
Requires-Dist: pillow
|
|
24
24
|
Requires-Dist: dotenv
|
|
25
|
+
Requires-Dist: lisette
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Dynamic: author
|
|
27
28
|
Dynamic: author-email
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -19,4 +19,11 @@ d = { 'settings': { 'branch': 'main',
|
|
|
19
19
|
'mistocr.core.save_pages': ('core.html#save_pages', 'mistocr/core.py'),
|
|
20
20
|
'mistocr.core.submit_batch': ('core.html#submit_batch', 'mistocr/core.py'),
|
|
21
21
|
'mistocr.core.upload_pdf': ('core.html#upload_pdf', 'mistocr/core.py'),
|
|
22
|
-
'mistocr.core.wait_for_job': ('core.html#wait_for_job', 'mistocr/core.py')}
|
|
22
|
+
'mistocr.core.wait_for_job': ('core.html#wait_for_job', 'mistocr/core.py')},
|
|
23
|
+
'mistocr.refine': { 'mistocr.refine.HeadingCorrections': ('refine.html#headingcorrections', 'mistocr/refine.py'),
|
|
24
|
+
'mistocr.refine.apply_hdg_fixes': ('refine.html#apply_hdg_fixes', 'mistocr/refine.py'),
|
|
25
|
+
'mistocr.refine.fix_hdg_hierarchy': ('refine.html#fix_hdg_hierarchy', 'mistocr/refine.py'),
|
|
26
|
+
'mistocr.refine.fix_md_hdgs': ('refine.html#fix_md_hdgs', 'mistocr/refine.py'),
|
|
27
|
+
'mistocr.refine.fmt_hdgs_idx': ('refine.html#fmt_hdgs_idx', 'mistocr/refine.py'),
|
|
28
|
+
'mistocr.refine.get_hdgs': ('refine.html#get_hdgs', 'mistocr/refine.py'),
|
|
29
|
+
'mistocr.refine.mk_fixes_lut': ('refine.html#mk_fixes_lut', 'mistocr/refine.py')}}}
|
|
@@ -110,11 +110,11 @@ def save_images(
|
|
|
110
110
|
# %% ../nbs/00_core.ipynb 32
|
|
111
111
|
def save_page(
|
|
112
112
|
page:dict, # Page dict,
|
|
113
|
-
|
|
113
|
+
dst:str, # Directory to save page
|
|
114
114
|
img_dir:str='img' # Directory to save images
|
|
115
115
|
) -> None:
|
|
116
116
|
"Save single page markdown and images"
|
|
117
|
-
(
|
|
117
|
+
(dst / f"page_{page['index']+1}.md").write_text(page['markdown'])
|
|
118
118
|
if page.get('images'):
|
|
119
119
|
img_dir.mkdir(exist_ok=True)
|
|
120
120
|
save_images(page, img_dir)
|
|
@@ -122,15 +122,15 @@ def save_page(
|
|
|
122
122
|
# %% ../nbs/00_core.ipynb 34
|
|
123
123
|
def save_pages(
|
|
124
124
|
ocr_resp:dict, # OCR response,
|
|
125
|
-
|
|
125
|
+
dst:str, # Directory to save pages,
|
|
126
126
|
cid:str # Custom ID
|
|
127
127
|
) -> Path: # Output directory
|
|
128
128
|
"Save markdown pages and images from OCR response to output directory"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
img_dir =
|
|
132
|
-
for page in ocr_resp['pages']: save_page(page,
|
|
133
|
-
return
|
|
129
|
+
dst = Path(dst) / cid
|
|
130
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
img_dir = dst / 'img'
|
|
132
|
+
for page in ocr_resp['pages']: save_page(page, dst, img_dir)
|
|
133
|
+
return dst
|
|
134
134
|
|
|
135
135
|
# %% ../nbs/00_core.ipynb 40
|
|
136
136
|
def _get_paths(path:str) -> list[Path]:
|
|
@@ -163,7 +163,7 @@ def _run_batch(entries:list[dict], c:Mistral, poll_interval:int=2) -> list[dict]
|
|
|
163
163
|
# %% ../nbs/00_core.ipynb 43
|
|
164
164
|
def ocr(
|
|
165
165
|
path:str, # Path to PDF file or folder,
|
|
166
|
-
|
|
166
|
+
dst:str='md', # Directory to save markdown pages,
|
|
167
167
|
inc_img:bool=True, # Include image in response,
|
|
168
168
|
key:str=None, # API key,
|
|
169
169
|
poll_interval:int=2 # Poll interval in seconds
|
|
@@ -172,18 +172,15 @@ def ocr(
|
|
|
172
172
|
pdfs = _get_paths(path)
|
|
173
173
|
entries, c = _prep_batch(pdfs, inc_img, key)
|
|
174
174
|
results = _run_batch(entries, c, poll_interval)
|
|
175
|
-
return L([save_pages(r['response']['body'],
|
|
175
|
+
return L([save_pages(r['response']['body'], dst, r['custom_id']) for r in results])
|
|
176
176
|
|
|
177
177
|
# %% ../nbs/00_core.ipynb 48
|
|
178
178
|
def read_pgs(
|
|
179
179
|
path:str, # OCR output directory,
|
|
180
|
-
|
|
181
|
-
) -> str:
|
|
180
|
+
join:bool=True # Join pages into single string
|
|
181
|
+
) -> str|list[str]: # Joined string or list of page contents
|
|
182
182
|
"Read specific page or all pages from OCR output directory"
|
|
183
183
|
path = Path(path)
|
|
184
|
-
if pg:
|
|
185
|
-
pg_path = path / f'page_{pg}.md'
|
|
186
|
-
if not pg_path.exists(): raise ValueError(f"Page {pg} not found")
|
|
187
|
-
return pg_path.read_text()
|
|
188
184
|
pgs = sorted(path.glob('page_*.md'), key=lambda p: int(p.stem.split('_')[1]))
|
|
189
|
-
|
|
185
|
+
contents = L([p.read_text() for p in pgs])
|
|
186
|
+
return '\n\n'.join(contents) if join else contents
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Postprocess markdown files by fixing heading hierarchy and describint images"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_refine.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto 0
|
|
6
|
+
__all__ = ['prompt_fix_hdgs', 'get_hdgs', 'fmt_hdgs_idx', 'HeadingCorrections', 'fix_hdg_hierarchy', 'mk_fixes_lut',
|
|
7
|
+
'apply_hdg_fixes', 'fix_md_hdgs']
|
|
8
|
+
|
|
9
|
+
# %% ../nbs/01_refine.ipynb 3
|
|
10
|
+
from fastcore.all import *
|
|
11
|
+
from .core import read_pgs
|
|
12
|
+
from re import sub, findall, MULTILINE
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from lisette.core import completion
|
|
15
|
+
import os
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
# %% ../nbs/01_refine.ipynb 7
|
|
19
|
+
def get_hdgs(
|
|
20
|
+
md:str # Markdown file string
|
|
21
|
+
):
|
|
22
|
+
"Return the markdown headings"
|
|
23
|
+
# Sanitize removing '#' in python snippet if any
|
|
24
|
+
md = sub(r'```[\s\S]*?```', '', md)
|
|
25
|
+
return L(findall(r'^#{1,6} .+$', md, MULTILINE))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# %% ../nbs/01_refine.ipynb 10
|
|
30
|
+
def fmt_hdgs_idx(
|
|
31
|
+
hdgs: list[str] # List of markdown headings
|
|
32
|
+
) -> str: # Formatted string with index
|
|
33
|
+
"Format the headings with index"
|
|
34
|
+
return '\n'.join(f"{i}. {h}" for i, h in enumerate(hdgs))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# %% ../nbs/01_refine.ipynb 13
|
|
38
|
+
class HeadingCorrections(BaseModel):
|
|
39
|
+
corrections: dict[int, str] # index → corrected heading
|
|
40
|
+
|
|
41
|
+
# %% ../nbs/01_refine.ipynb 15
|
|
42
|
+
prompt_fix_hdgs = """Fix markdown heading hierarchy errors while preserving the document's intended structure.
|
|
43
|
+
|
|
44
|
+
INPUT FORMAT: Each heading is prefixed with its index number (e.g., "0. # Title")
|
|
45
|
+
|
|
46
|
+
RULES - Only fix these errors:
|
|
47
|
+
1. **Level jumps**: Headings can only increase by one # at a time
|
|
48
|
+
- Wrong: 0. # Title → 1. #### Abstract
|
|
49
|
+
- Fixed: 0. # Title → 1. ## Abstract
|
|
50
|
+
|
|
51
|
+
2. **Numbering inconsistency**: Subsection numbers must be one level deeper
|
|
52
|
+
- Wrong: 4. ## 3. Section → 5. ## 3.1 Subsection
|
|
53
|
+
- Fixed: 4. ## 3. Section → 5. ### 3.1 Subsection
|
|
54
|
+
|
|
55
|
+
3. **Preserve working structure**: If sections are consistently marked, keep it
|
|
56
|
+
|
|
57
|
+
4. **Decreasing levels is OK**: Going from ### to ## is valid for new sections
|
|
58
|
+
|
|
59
|
+
OUTPUT: Return a Python dictionary mapping index to corrected heading (without the index prefix).
|
|
60
|
+
Only include entries that need changes. Example: {{1: '## Abstract', 15: '### PASCAL VOC'}}
|
|
61
|
+
|
|
62
|
+
Headings to analyze:
|
|
63
|
+
{headings_list}
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# %% ../nbs/01_refine.ipynb 16
|
|
67
|
+
def fix_hdg_hierarchy(
|
|
68
|
+
hdgs: list[str], # List of markdown headings
|
|
69
|
+
model: str='claude-sonnet-4-5', # Model to use
|
|
70
|
+
api_key: str=os.getenv('ANTHROPIC_API_KEY') # API key
|
|
71
|
+
) -> dict[int, str]: # Dictionary of index → corrected heading
|
|
72
|
+
"Fix the heading hierarchy"
|
|
73
|
+
r = completion(
|
|
74
|
+
model=model,
|
|
75
|
+
messages=[{"role": "user", "content": prompt_fix_hdgs.format(headings_list=fmt_hdgs_idx(hdgs))}],
|
|
76
|
+
response_format=HeadingCorrections,
|
|
77
|
+
api_key=api_key
|
|
78
|
+
)
|
|
79
|
+
return json.loads(r.choices[0].message.content)['corrections']
|
|
80
|
+
|
|
81
|
+
# %% ../nbs/01_refine.ipynb 19
|
|
82
|
+
def mk_fixes_lut(
|
|
83
|
+
hdgs: list[str], # List of markdown headings
|
|
84
|
+
model: str='claude-sonnet-4-5', # Model to use
|
|
85
|
+
api_key: str=os.getenv('ANTHROPIC_API_KEY') # API key
|
|
86
|
+
) -> dict[str, str]: # Dictionary of old → new heading
|
|
87
|
+
"Make a lookup table of fixes"
|
|
88
|
+
fixes = fix_hdg_hierarchy(hdgs, model, api_key)
|
|
89
|
+
return {hdgs[int(k)]:v for k,v in fixes.items()}
|
|
90
|
+
|
|
91
|
+
# %% ../nbs/01_refine.ipynb 22
|
|
92
|
+
def apply_hdg_fixes(
|
|
93
|
+
p:str, # Page to fix
|
|
94
|
+
lut_fixes: dict[str, str], # Lookup table of fixes
|
|
95
|
+
pg: int=None, # Optionnaly specify the page number to append to original heading
|
|
96
|
+
) -> str: # Page with fixes applied
|
|
97
|
+
"Apply the fixes to the page"
|
|
98
|
+
for old in get_hdgs(p): p = p.replace(old, lut_fixes.get(old, old) + (f' .... page {pg}' if pg else ''))
|
|
99
|
+
return p
|
|
100
|
+
|
|
101
|
+
# %% ../nbs/01_refine.ipynb 25
|
|
102
|
+
def fix_md_hdgs(
|
|
103
|
+
src:str, # Source directory with markdown pages
|
|
104
|
+
model:str='claude-sonnet-4-5', # Model
|
|
105
|
+
dst:str=None, # Destination directory (None=overwrite)
|
|
106
|
+
pg_nums:bool=True # Add page numbers
|
|
107
|
+
):
|
|
108
|
+
"Fix heading hierarchy in markdown document"
|
|
109
|
+
src_path,dst_path = Path(src),Path(dst) if dst else Path(src)
|
|
110
|
+
if dst_path != src_path: dst_path.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
lut = mk_fixes_lut(get_hdgs(read_pgs(src_path)), model)
|
|
112
|
+
for i,p in enumerate(read_pgs(src_path, join=False), 1):
|
|
113
|
+
(dst_path/f'page_{i}.md').write_text(apply_hdg_fixes(p, lut, pg=i if pg_nums else None))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mistocr
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Simple batch OCR for PDFs using Mistral's state-of-the-art vision model
|
|
5
5
|
Home-page: https://github.com/franckalbinet/mistocr
|
|
6
6
|
Author: Solveit
|
|
@@ -22,6 +22,7 @@ Requires-Dist: fastcore
|
|
|
22
22
|
Requires-Dist: mistralai
|
|
23
23
|
Requires-Dist: pillow
|
|
24
24
|
Requires-Dist: dotenv
|
|
25
|
+
Requires-Dist: lisette
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Dynamic: author
|
|
27
28
|
Dynamic: author-email
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[DEFAULT]
|
|
2
2
|
repo = mistocr
|
|
3
3
|
lib_name = mistocr
|
|
4
|
-
version = 0.0
|
|
4
|
+
version = 0.1.0
|
|
5
5
|
min_python = 3.9
|
|
6
6
|
license = apache2
|
|
7
7
|
black_formatting = False
|
|
@@ -27,7 +27,7 @@ keywords = nbdev jupyter notebook python
|
|
|
27
27
|
language = English
|
|
28
28
|
status = 3
|
|
29
29
|
user = franckalbinet
|
|
30
|
-
requirements = fastcore mistralai pillow dotenv
|
|
30
|
+
requirements = fastcore mistralai pillow dotenv lisette
|
|
31
31
|
readme_nb = index.ipynb
|
|
32
32
|
allowed_metadata_keys =
|
|
33
33
|
allowed_cell_metadata_keys =
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|