newspaper-ocr 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.
- newspaper_ocr-0.1.0/LICENSE +21 -0
- newspaper_ocr-0.1.0/PKG-INFO +243 -0
- newspaper_ocr-0.1.0/README.md +201 -0
- newspaper_ocr-0.1.0/pyproject.toml +52 -0
- newspaper_ocr-0.1.0/setup.cfg +4 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/__init__.py +5 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/cli.py +87 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/detectors/__init__.py +12 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/detectors/as_yolo.py +502 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/detectors/base.py +10 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/detectors/paddlex.py +59 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/formatters/__init__.py +10 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/formatters/base.py +9 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/formatters/hocr.py +29 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/formatters/json_fmt.py +29 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/formatters/text.py +7 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/layout_processor.py +504 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/models.py +53 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/pipeline.py +147 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/recognizers/__init__.py +28 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/recognizers/base.py +19 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/recognizers/effocr.py +47 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/recognizers/glm_ocr.py +198 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/recognizers/tesseract.py +72 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/recognizers/tesserocr_backend.py +100 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/registry.py +19 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/spell_checker.py +171 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr/text_cleaner.py +181 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr.egg-info/PKG-INFO +243 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr.egg-info/SOURCES.txt +40 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr.egg-info/dependency_links.txt +1 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr.egg-info/entry_points.txt +2 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr.egg-info/requires.txt +25 -0
- newspaper_ocr-0.1.0/src/newspaper_ocr.egg-info/top_level.txt +1 -0
- newspaper_ocr-0.1.0/tests/test_cli.py +16 -0
- newspaper_ocr-0.1.0/tests/test_layout_processor.py +340 -0
- newspaper_ocr-0.1.0/tests/test_models.py +44 -0
- newspaper_ocr-0.1.0/tests/test_pipeline.py +208 -0
- newspaper_ocr-0.1.0/tests/test_pipeline_factory.py +28 -0
- newspaper_ocr-0.1.0/tests/test_registry.py +21 -0
- newspaper_ocr-0.1.0/tests/test_spell_checker.py +246 -0
- newspaper_ocr-0.1.0/tests/test_text_cleaner.py +298 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Neal Caren
|
|
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,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: newspaper-ocr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Modular OCR pipeline for historical newspaper scans
|
|
5
|
+
Author-email: Neal Caren <neal.caren@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nealcaren/newspaper-ocr
|
|
8
|
+
Project-URL: Repository, https://github.com/nealcaren/newspaper-ocr
|
|
9
|
+
Project-URL: Issues, https://github.com/nealcaren/newspaper-ocr/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
17
|
+
Classifier: Topic :: Text Processing :: General
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pillow>=10.0
|
|
22
|
+
Requires-Dist: opencv-python-headless>=4.8
|
|
23
|
+
Requires-Dist: onnxruntime>=1.16
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: torch>=2.0
|
|
26
|
+
Requires-Dist: torchvision>=0.15
|
|
27
|
+
Requires-Dist: click>=8.0
|
|
28
|
+
Requires-Dist: huggingface-hub>=0.20
|
|
29
|
+
Provides-Extra: tesseract
|
|
30
|
+
Provides-Extra: paddlex
|
|
31
|
+
Requires-Dist: paddlepaddle; extra == "paddlex"
|
|
32
|
+
Requires-Dist: paddlex; extra == "paddlex"
|
|
33
|
+
Provides-Extra: glm-ocr
|
|
34
|
+
Requires-Dist: transformers>=5.1; extra == "glm-ocr"
|
|
35
|
+
Requires-Dist: torch; extra == "glm-ocr"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: newspaper-ocr[glm-ocr,paddlex,tesseract]; extra == "all"
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# newspaper-ocr
|
|
44
|
+
|
|
45
|
+
Modular OCR pipeline for historical newspaper scans. Three-phase architecture with swappable backends at every stage.
|
|
46
|
+
|
|
47
|
+
## Pipeline
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Phase 1 Phase 2 Phase 3
|
|
51
|
+
LAYOUT OCR POST-PROCESSING
|
|
52
|
+
|
|
53
|
+
Image ──→ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
|
|
54
|
+
│ Detection │ │ Recognition │ │ Text Cleaning │
|
|
55
|
+
JP2 │ (AS YOLO or │──→│ (Tesseract, │──→│ (dehyphenation, │──→ Output
|
|
56
|
+
JPG │ PP-DocLayout) │ │ tesserocr, │ │ line joining) │ text
|
|
57
|
+
PNG │ │ │ EffOCR) │ │ │ json
|
|
58
|
+
│ Layout Proc. │ │ │ │ Spell Check │ hOCR
|
|
59
|
+
│ (reading order, │ │ │ │ (SymSpell) │
|
|
60
|
+
│ dedup, merge) │ │ │ │ │
|
|
61
|
+
└─────────────────┘ └──────────────┘ └─────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Phase 1 — Layout:** Detect regions (articles, headlines, ads) and text lines. Reorder into newspaper reading order (columns left-to-right, top-to-bottom). Deduplicate overlapping detections, fill gaps.
|
|
65
|
+
|
|
66
|
+
**Phase 2 — OCR:** Recognize text in each detected line or region. Swappable backends with different speed/accuracy tradeoffs.
|
|
67
|
+
|
|
68
|
+
**Phase 3 — Post-Processing:** Reconstruct continuous text from OCR'd lines. Rejoin hyphenated words across line breaks. Join continuation lines into paragraphs. Optional spell correction.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install newspaper-ocr
|
|
74
|
+
|
|
75
|
+
# Tesseract (requires system install):
|
|
76
|
+
# macOS: brew install tesseract
|
|
77
|
+
# Ubuntu: apt install tesseract-ocr
|
|
78
|
+
|
|
79
|
+
# Optional backends:
|
|
80
|
+
pip install "newspaper-ocr[glm-ocr]" # GLM-OCR vision-language model
|
|
81
|
+
pip install "newspaper-ocr[paddlex]" # PP-DocLayout detector
|
|
82
|
+
|
|
83
|
+
# EfficientOCR (installed separately from fork):
|
|
84
|
+
pip install git+https://github.com/nealcaren/efficient_ocr.git
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick Start
|
|
88
|
+
|
|
89
|
+
### Python
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from newspaper_ocr import Pipeline
|
|
93
|
+
|
|
94
|
+
# Defaults: AS YOLO detection + Tesseract recognition
|
|
95
|
+
pipe = Pipeline()
|
|
96
|
+
text = pipe.ocr("page.jp2")
|
|
97
|
+
|
|
98
|
+
# Fast mode (tesserocr C API, ~4x faster)
|
|
99
|
+
pipe = Pipeline(recognizer="tesserocr")
|
|
100
|
+
|
|
101
|
+
# With spell correction
|
|
102
|
+
pipe = Pipeline(recognizer="tesserocr", spell_check=True)
|
|
103
|
+
|
|
104
|
+
# JSON output with bounding boxes and confidence scores
|
|
105
|
+
pipe = Pipeline(output="json")
|
|
106
|
+
result = pipe.ocr("page.jp2")
|
|
107
|
+
|
|
108
|
+
# Fine-tuned Tesseract model
|
|
109
|
+
from newspaper_ocr.recognizers.tesseract import TesseractRecognizer
|
|
110
|
+
rec = TesseractRecognizer(model="news_gold_v2", tessdata_dir="/path/to/models")
|
|
111
|
+
pipe = Pipeline(recognizer=rec)
|
|
112
|
+
|
|
113
|
+
# Disable layout post-processing (for non-newspaper documents)
|
|
114
|
+
pipe = Pipeline(layout_processing=False)
|
|
115
|
+
|
|
116
|
+
# Batch processing
|
|
117
|
+
results = pipe.ocr_batch(["page1.jp2", "page2.jp2", "page3.jp2"])
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Command Line
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Basic OCR
|
|
124
|
+
newspaper-ocr page.jp2
|
|
125
|
+
|
|
126
|
+
# Fast mode with JSON output
|
|
127
|
+
newspaper-ocr page.jp2 --backend tesserocr --output json
|
|
128
|
+
|
|
129
|
+
# With spell correction
|
|
130
|
+
newspaper-ocr page.jp2 --backend tesserocr --spell-check
|
|
131
|
+
|
|
132
|
+
# Batch processing to files
|
|
133
|
+
newspaper-ocr *.jp2 --outdir results/ --output text
|
|
134
|
+
|
|
135
|
+
# Fine-tuned model
|
|
136
|
+
newspaper-ocr page.jp2 --model news_gold_v2.traineddata
|
|
137
|
+
|
|
138
|
+
# Disable post-processing
|
|
139
|
+
newspaper-ocr page.jp2 --no-layout-processing --no-text-cleaning
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Phase 1: Layout
|
|
143
|
+
|
|
144
|
+
Two detection backends, plus battle-tested newspaper layout post-processing.
|
|
145
|
+
|
|
146
|
+
### Detectors
|
|
147
|
+
|
|
148
|
+
| Detector | What it finds | Speed | Best for |
|
|
149
|
+
|----------|--------------|-------|----------|
|
|
150
|
+
| `as_yolo` (default) | Regions + lines | ~8s/page | Line-level OCR (Tesseract, EffOCR) |
|
|
151
|
+
| `paddlex` | Regions only (20 categories) | varies | Region-level OCR, detailed layout analysis |
|
|
152
|
+
|
|
153
|
+
### Layout Processing
|
|
154
|
+
|
|
155
|
+
Ported from the [Dangerous Press](https://dangerouspress.org) production pipeline. Applied automatically after detection:
|
|
156
|
+
|
|
157
|
+
1. **Filter** low-confidence detections
|
|
158
|
+
2. **Rescue** missed regions in gaps between accepted detections
|
|
159
|
+
3. **Deduplicate** overlapping regions (three-pass: contained, title-text, near-duplicate)
|
|
160
|
+
4. **Fill column gaps** using geometric column detection
|
|
161
|
+
5. **Reading order** — column-aware sorting (full-width headers first, then column-by-column)
|
|
162
|
+
6. **Merge** vertically adjacent blocks into coherent regions
|
|
163
|
+
|
|
164
|
+
Disable with `layout_processing=False`.
|
|
165
|
+
|
|
166
|
+
## Phase 2: OCR
|
|
167
|
+
|
|
168
|
+
Three recognition backends with different speed/accuracy tradeoffs.
|
|
169
|
+
|
|
170
|
+
| Backend | Mode | Speed | CER* | How it works |
|
|
171
|
+
|---------|------|-------|------|-------------|
|
|
172
|
+
| `tesseract` | line | ~106s | 3.2% | Subprocess per line, LSTM sequence model |
|
|
173
|
+
| `tesseract` | region | ~38s | — | Subprocess per region, Tesseract's own line segmentation |
|
|
174
|
+
| `tesserocr` | line | ~26s | 3.2% | C API bindings, no subprocess overhead |
|
|
175
|
+
| `tesserocr` | region | ~25s | — | C API, region-level |
|
|
176
|
+
| `effocr` | line | ~50s | 11.2% | Contrastive char/word matching, ONNX |
|
|
177
|
+
|
|
178
|
+
*CER measured against LLM gold-standard labels with fine-tuned `news_gold_v2` model. Baseline Tesseract (eng) is ~8-11% CER. Times on a single newspaper page (~1,100 lines).
|
|
179
|
+
|
|
180
|
+
### Fine-Tuned Models
|
|
181
|
+
|
|
182
|
+
The pipeline includes infrastructure for fine-tuning Tesseract on historical newspaper text using LLM-verified gold-standard labels. See `dangerouspress-ocr-finetune` for the training pipeline.
|
|
183
|
+
|
|
184
|
+
## Phase 3: Post-Processing
|
|
185
|
+
|
|
186
|
+
### Text Cleaning
|
|
187
|
+
|
|
188
|
+
Reconstructs continuous text from OCR'd lines:
|
|
189
|
+
|
|
190
|
+
- **Dehyphenation**: `"com-" + "plete"` → `"complete"` (when next line starts lowercase)
|
|
191
|
+
- **Line joining**: Continuation lines joined with spaces
|
|
192
|
+
- **Paragraph breaks**: Detected via vertical gaps, column shifts, or terminal punctuation + uppercase
|
|
193
|
+
- **Semantic dashes preserved**: Em-dashes and spaced dashes kept intact
|
|
194
|
+
|
|
195
|
+
Disable with `text_cleaning=False` or `--no-text-cleaning`.
|
|
196
|
+
|
|
197
|
+
### Spell Correction
|
|
198
|
+
|
|
199
|
+
Optional SymSpell-based correction (`spell_check=True`):
|
|
200
|
+
|
|
201
|
+
- Corrects words not found in dictionary (edit distance ≤ 2)
|
|
202
|
+
- Preserves capitalization, punctuation, numbers, abbreviations
|
|
203
|
+
- Supports custom frequency dictionaries for corpus-specific vocabulary
|
|
204
|
+
- Logs all corrections for review
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
pipe = Pipeline(spell_check=True)
|
|
208
|
+
|
|
209
|
+
# With corpus-specific dictionary
|
|
210
|
+
from newspaper_ocr.spell_checker import SpellChecker
|
|
211
|
+
checker = SpellChecker(dictionary_path="my_newspaper_words.txt")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Output Formats
|
|
215
|
+
|
|
216
|
+
| Format | Flag | Content |
|
|
217
|
+
|--------|------|---------|
|
|
218
|
+
| `text` | `--output text` | Plain text, paragraphs separated by blank lines |
|
|
219
|
+
| `json` | `--output json` | Structured: regions, lines, bounding boxes, confidence |
|
|
220
|
+
| `hocr` | `--output hocr` | HTML with spatial coordinates (for text overlay on images) |
|
|
221
|
+
|
|
222
|
+
## Architecture
|
|
223
|
+
|
|
224
|
+
Every stage is a swappable component behind an abstract interface. Adding a new backend = one file + one registry entry.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# Custom detector
|
|
228
|
+
from newspaper_ocr.detectors.base import Detector
|
|
229
|
+
class MyDetector(Detector):
|
|
230
|
+
def detect(self, image) -> PageLayout: ...
|
|
231
|
+
|
|
232
|
+
# Custom recognizer
|
|
233
|
+
from newspaper_ocr.recognizers.base import LineRecognizer
|
|
234
|
+
class MyRecognizer(LineRecognizer):
|
|
235
|
+
def recognize(self, line) -> Line: ...
|
|
236
|
+
|
|
237
|
+
# Plug into pipeline
|
|
238
|
+
pipe = Pipeline(detector=MyDetector(), recognizer=MyRecognizer())
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# newspaper-ocr
|
|
2
|
+
|
|
3
|
+
Modular OCR pipeline for historical newspaper scans. Three-phase architecture with swappable backends at every stage.
|
|
4
|
+
|
|
5
|
+
## Pipeline
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Phase 1 Phase 2 Phase 3
|
|
9
|
+
LAYOUT OCR POST-PROCESSING
|
|
10
|
+
|
|
11
|
+
Image ──→ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
|
|
12
|
+
│ Detection │ │ Recognition │ │ Text Cleaning │
|
|
13
|
+
JP2 │ (AS YOLO or │──→│ (Tesseract, │──→│ (dehyphenation, │──→ Output
|
|
14
|
+
JPG │ PP-DocLayout) │ │ tesserocr, │ │ line joining) │ text
|
|
15
|
+
PNG │ │ │ EffOCR) │ │ │ json
|
|
16
|
+
│ Layout Proc. │ │ │ │ Spell Check │ hOCR
|
|
17
|
+
│ (reading order, │ │ │ │ (SymSpell) │
|
|
18
|
+
│ dedup, merge) │ │ │ │ │
|
|
19
|
+
└─────────────────┘ └──────────────┘ └─────────────────┘
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Phase 1 — Layout:** Detect regions (articles, headlines, ads) and text lines. Reorder into newspaper reading order (columns left-to-right, top-to-bottom). Deduplicate overlapping detections, fill gaps.
|
|
23
|
+
|
|
24
|
+
**Phase 2 — OCR:** Recognize text in each detected line or region. Swappable backends with different speed/accuracy tradeoffs.
|
|
25
|
+
|
|
26
|
+
**Phase 3 — Post-Processing:** Reconstruct continuous text from OCR'd lines. Rejoin hyphenated words across line breaks. Join continuation lines into paragraphs. Optional spell correction.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install newspaper-ocr
|
|
32
|
+
|
|
33
|
+
# Tesseract (requires system install):
|
|
34
|
+
# macOS: brew install tesseract
|
|
35
|
+
# Ubuntu: apt install tesseract-ocr
|
|
36
|
+
|
|
37
|
+
# Optional backends:
|
|
38
|
+
pip install "newspaper-ocr[glm-ocr]" # GLM-OCR vision-language model
|
|
39
|
+
pip install "newspaper-ocr[paddlex]" # PP-DocLayout detector
|
|
40
|
+
|
|
41
|
+
# EfficientOCR (installed separately from fork):
|
|
42
|
+
pip install git+https://github.com/nealcaren/efficient_ocr.git
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Python
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from newspaper_ocr import Pipeline
|
|
51
|
+
|
|
52
|
+
# Defaults: AS YOLO detection + Tesseract recognition
|
|
53
|
+
pipe = Pipeline()
|
|
54
|
+
text = pipe.ocr("page.jp2")
|
|
55
|
+
|
|
56
|
+
# Fast mode (tesserocr C API, ~4x faster)
|
|
57
|
+
pipe = Pipeline(recognizer="tesserocr")
|
|
58
|
+
|
|
59
|
+
# With spell correction
|
|
60
|
+
pipe = Pipeline(recognizer="tesserocr", spell_check=True)
|
|
61
|
+
|
|
62
|
+
# JSON output with bounding boxes and confidence scores
|
|
63
|
+
pipe = Pipeline(output="json")
|
|
64
|
+
result = pipe.ocr("page.jp2")
|
|
65
|
+
|
|
66
|
+
# Fine-tuned Tesseract model
|
|
67
|
+
from newspaper_ocr.recognizers.tesseract import TesseractRecognizer
|
|
68
|
+
rec = TesseractRecognizer(model="news_gold_v2", tessdata_dir="/path/to/models")
|
|
69
|
+
pipe = Pipeline(recognizer=rec)
|
|
70
|
+
|
|
71
|
+
# Disable layout post-processing (for non-newspaper documents)
|
|
72
|
+
pipe = Pipeline(layout_processing=False)
|
|
73
|
+
|
|
74
|
+
# Batch processing
|
|
75
|
+
results = pipe.ocr_batch(["page1.jp2", "page2.jp2", "page3.jp2"])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Command Line
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Basic OCR
|
|
82
|
+
newspaper-ocr page.jp2
|
|
83
|
+
|
|
84
|
+
# Fast mode with JSON output
|
|
85
|
+
newspaper-ocr page.jp2 --backend tesserocr --output json
|
|
86
|
+
|
|
87
|
+
# With spell correction
|
|
88
|
+
newspaper-ocr page.jp2 --backend tesserocr --spell-check
|
|
89
|
+
|
|
90
|
+
# Batch processing to files
|
|
91
|
+
newspaper-ocr *.jp2 --outdir results/ --output text
|
|
92
|
+
|
|
93
|
+
# Fine-tuned model
|
|
94
|
+
newspaper-ocr page.jp2 --model news_gold_v2.traineddata
|
|
95
|
+
|
|
96
|
+
# Disable post-processing
|
|
97
|
+
newspaper-ocr page.jp2 --no-layout-processing --no-text-cleaning
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Phase 1: Layout
|
|
101
|
+
|
|
102
|
+
Two detection backends, plus battle-tested newspaper layout post-processing.
|
|
103
|
+
|
|
104
|
+
### Detectors
|
|
105
|
+
|
|
106
|
+
| Detector | What it finds | Speed | Best for |
|
|
107
|
+
|----------|--------------|-------|----------|
|
|
108
|
+
| `as_yolo` (default) | Regions + lines | ~8s/page | Line-level OCR (Tesseract, EffOCR) |
|
|
109
|
+
| `paddlex` | Regions only (20 categories) | varies | Region-level OCR, detailed layout analysis |
|
|
110
|
+
|
|
111
|
+
### Layout Processing
|
|
112
|
+
|
|
113
|
+
Ported from the [Dangerous Press](https://dangerouspress.org) production pipeline. Applied automatically after detection:
|
|
114
|
+
|
|
115
|
+
1. **Filter** low-confidence detections
|
|
116
|
+
2. **Rescue** missed regions in gaps between accepted detections
|
|
117
|
+
3. **Deduplicate** overlapping regions (three-pass: contained, title-text, near-duplicate)
|
|
118
|
+
4. **Fill column gaps** using geometric column detection
|
|
119
|
+
5. **Reading order** — column-aware sorting (full-width headers first, then column-by-column)
|
|
120
|
+
6. **Merge** vertically adjacent blocks into coherent regions
|
|
121
|
+
|
|
122
|
+
Disable with `layout_processing=False`.
|
|
123
|
+
|
|
124
|
+
## Phase 2: OCR
|
|
125
|
+
|
|
126
|
+
Three recognition backends with different speed/accuracy tradeoffs.
|
|
127
|
+
|
|
128
|
+
| Backend | Mode | Speed | CER* | How it works |
|
|
129
|
+
|---------|------|-------|------|-------------|
|
|
130
|
+
| `tesseract` | line | ~106s | 3.2% | Subprocess per line, LSTM sequence model |
|
|
131
|
+
| `tesseract` | region | ~38s | — | Subprocess per region, Tesseract's own line segmentation |
|
|
132
|
+
| `tesserocr` | line | ~26s | 3.2% | C API bindings, no subprocess overhead |
|
|
133
|
+
| `tesserocr` | region | ~25s | — | C API, region-level |
|
|
134
|
+
| `effocr` | line | ~50s | 11.2% | Contrastive char/word matching, ONNX |
|
|
135
|
+
|
|
136
|
+
*CER measured against LLM gold-standard labels with fine-tuned `news_gold_v2` model. Baseline Tesseract (eng) is ~8-11% CER. Times on a single newspaper page (~1,100 lines).
|
|
137
|
+
|
|
138
|
+
### Fine-Tuned Models
|
|
139
|
+
|
|
140
|
+
The pipeline includes infrastructure for fine-tuning Tesseract on historical newspaper text using LLM-verified gold-standard labels. See `dangerouspress-ocr-finetune` for the training pipeline.
|
|
141
|
+
|
|
142
|
+
## Phase 3: Post-Processing
|
|
143
|
+
|
|
144
|
+
### Text Cleaning
|
|
145
|
+
|
|
146
|
+
Reconstructs continuous text from OCR'd lines:
|
|
147
|
+
|
|
148
|
+
- **Dehyphenation**: `"com-" + "plete"` → `"complete"` (when next line starts lowercase)
|
|
149
|
+
- **Line joining**: Continuation lines joined with spaces
|
|
150
|
+
- **Paragraph breaks**: Detected via vertical gaps, column shifts, or terminal punctuation + uppercase
|
|
151
|
+
- **Semantic dashes preserved**: Em-dashes and spaced dashes kept intact
|
|
152
|
+
|
|
153
|
+
Disable with `text_cleaning=False` or `--no-text-cleaning`.
|
|
154
|
+
|
|
155
|
+
### Spell Correction
|
|
156
|
+
|
|
157
|
+
Optional SymSpell-based correction (`spell_check=True`):
|
|
158
|
+
|
|
159
|
+
- Corrects words not found in dictionary (edit distance ≤ 2)
|
|
160
|
+
- Preserves capitalization, punctuation, numbers, abbreviations
|
|
161
|
+
- Supports custom frequency dictionaries for corpus-specific vocabulary
|
|
162
|
+
- Logs all corrections for review
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
pipe = Pipeline(spell_check=True)
|
|
166
|
+
|
|
167
|
+
# With corpus-specific dictionary
|
|
168
|
+
from newspaper_ocr.spell_checker import SpellChecker
|
|
169
|
+
checker = SpellChecker(dictionary_path="my_newspaper_words.txt")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Output Formats
|
|
173
|
+
|
|
174
|
+
| Format | Flag | Content |
|
|
175
|
+
|--------|------|---------|
|
|
176
|
+
| `text` | `--output text` | Plain text, paragraphs separated by blank lines |
|
|
177
|
+
| `json` | `--output json` | Structured: regions, lines, bounding boxes, confidence |
|
|
178
|
+
| `hocr` | `--output hocr` | HTML with spatial coordinates (for text overlay on images) |
|
|
179
|
+
|
|
180
|
+
## Architecture
|
|
181
|
+
|
|
182
|
+
Every stage is a swappable component behind an abstract interface. Adding a new backend = one file + one registry entry.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# Custom detector
|
|
186
|
+
from newspaper_ocr.detectors.base import Detector
|
|
187
|
+
class MyDetector(Detector):
|
|
188
|
+
def detect(self, image) -> PageLayout: ...
|
|
189
|
+
|
|
190
|
+
# Custom recognizer
|
|
191
|
+
from newspaper_ocr.recognizers.base import LineRecognizer
|
|
192
|
+
class MyRecognizer(LineRecognizer):
|
|
193
|
+
def recognize(self, line) -> Line: ...
|
|
194
|
+
|
|
195
|
+
# Plug into pipeline
|
|
196
|
+
pipe = Pipeline(detector=MyDetector(), recognizer=MyRecognizer())
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "newspaper-ocr"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Modular OCR pipeline for historical newspaper scans"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Neal Caren", email = "neal.caren@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
23
|
+
"Topic :: Text Processing :: General",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"pillow>=10.0",
|
|
27
|
+
"opencv-python-headless>=4.8",
|
|
28
|
+
"onnxruntime>=1.16",
|
|
29
|
+
"numpy>=1.24",
|
|
30
|
+
"torch>=2.0",
|
|
31
|
+
"torchvision>=0.15",
|
|
32
|
+
"click>=8.0",
|
|
33
|
+
"huggingface-hub>=0.20",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/nealcaren/newspaper-ocr"
|
|
38
|
+
Repository = "https://github.com/nealcaren/newspaper-ocr"
|
|
39
|
+
Issues = "https://github.com/nealcaren/newspaper-ocr/issues"
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
tesseract = []
|
|
43
|
+
paddlex = ["paddlepaddle", "paddlex"]
|
|
44
|
+
glm-ocr = ["transformers>=5.1", "torch"]
|
|
45
|
+
all = ["newspaper-ocr[tesseract,paddlex,glm-ocr]"]
|
|
46
|
+
dev = ["pytest>=7.0", "pytest-cov"]
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
newspaper-ocr = "newspaper_ocr.cli:main"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""CLI entry point for newspaper-ocr."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
@click.argument("images", nargs=-1, required=True, type=click.Path(exists=True))
|
|
10
|
+
@click.option("--backend", "-b", default="tesseract",
|
|
11
|
+
help="Recognition backend: tesseract, tesserocr, effocr")
|
|
12
|
+
@click.option("--detector", "-d", default="as_yolo",
|
|
13
|
+
help="Detection backend: as_yolo, paddlex")
|
|
14
|
+
@click.option("--output", "-o", default="text",
|
|
15
|
+
help="Output format: text, json, hocr")
|
|
16
|
+
@click.option("--model", "-m", default=None,
|
|
17
|
+
help="Custom model path (e.g., traineddata for Tesseract)")
|
|
18
|
+
@click.option("--model-dir", default=None,
|
|
19
|
+
help="Model cache directory")
|
|
20
|
+
@click.option("--mode", default="region",
|
|
21
|
+
help="Recognition mode: line or region (default: region)")
|
|
22
|
+
@click.option("--no-layout-processing", is_flag=True,
|
|
23
|
+
help="Disable reading order post-processing")
|
|
24
|
+
@click.option("--no-text-cleaning", is_flag=True,
|
|
25
|
+
help="Disable dehyphenation and line-joining post-processing")
|
|
26
|
+
@click.option("--spell-check", is_flag=True, default=False,
|
|
27
|
+
help="Enable SymSpell spell correction post-processing (off by default)")
|
|
28
|
+
@click.option("--fallback", default=None,
|
|
29
|
+
help="Fallback recognizer for low-confidence lines (e.g. glm-ocr)")
|
|
30
|
+
@click.option("--fallback-threshold", default=70, type=float,
|
|
31
|
+
help="Confidence threshold (0-100) below which fallback is used (default: 70)")
|
|
32
|
+
@click.option("--outdir", default=None,
|
|
33
|
+
help="Output directory (default: stdout)")
|
|
34
|
+
def main(images, backend, detector, output, model, model_dir, mode, no_layout_processing, no_text_cleaning, spell_check, fallback, fallback_threshold, outdir):
|
|
35
|
+
"""OCR historical newspaper scans.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
newspaper-ocr page.jp2
|
|
39
|
+
newspaper-ocr page.jp2 --backend tesserocr --output json
|
|
40
|
+
newspaper-ocr *.jp2 --outdir results/ --output text
|
|
41
|
+
newspaper-ocr page.jp2 --model news_gold_v2.traineddata
|
|
42
|
+
"""
|
|
43
|
+
from newspaper_ocr import Pipeline
|
|
44
|
+
|
|
45
|
+
# Build recognizer with mode
|
|
46
|
+
from newspaper_ocr.recognizers import RECOGNIZERS
|
|
47
|
+
import inspect
|
|
48
|
+
rec_cls = RECOGNIZERS.get(backend)
|
|
49
|
+
rec_kwargs = {}
|
|
50
|
+
params = inspect.signature(rec_cls.__init__).parameters
|
|
51
|
+
if "mode" in params:
|
|
52
|
+
rec_kwargs["mode"] = mode
|
|
53
|
+
if model:
|
|
54
|
+
# Route model arg to the right param
|
|
55
|
+
if "model" in params:
|
|
56
|
+
rec_kwargs["model"] = model
|
|
57
|
+
elif "model_dir" in params:
|
|
58
|
+
rec_kwargs["model_dir"] = model
|
|
59
|
+
|
|
60
|
+
recognizer = rec_cls(**rec_kwargs)
|
|
61
|
+
|
|
62
|
+
pipe = Pipeline(
|
|
63
|
+
detector=detector,
|
|
64
|
+
recognizer=recognizer,
|
|
65
|
+
output=output,
|
|
66
|
+
model_cache_dir=model_dir,
|
|
67
|
+
layout_processing=not no_layout_processing,
|
|
68
|
+
text_cleaning=not no_text_cleaning,
|
|
69
|
+
spell_check=spell_check,
|
|
70
|
+
fallback=fallback,
|
|
71
|
+
fallback_threshold=fallback_threshold,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
for image_path in images:
|
|
75
|
+
result = pipe.ocr(image_path)
|
|
76
|
+
|
|
77
|
+
if outdir:
|
|
78
|
+
out_path = Path(outdir) / (Path(image_path).stem + _ext(output))
|
|
79
|
+
Path(outdir).mkdir(parents=True, exist_ok=True)
|
|
80
|
+
out_path.write_text(result)
|
|
81
|
+
click.echo(f"Saved: {out_path}", err=True)
|
|
82
|
+
else:
|
|
83
|
+
click.echo(result)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _ext(fmt: str) -> str:
|
|
87
|
+
return {"text": ".txt", "json": ".json", "hocr": ".hocr"}.get(fmt, ".txt")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from newspaper_ocr.detectors.base import Detector
|
|
2
|
+
from newspaper_ocr.detectors.as_yolo import AsYoloDetector
|
|
3
|
+
from newspaper_ocr.registry import Registry
|
|
4
|
+
|
|
5
|
+
DETECTORS = Registry("detector")
|
|
6
|
+
DETECTORS.register("as_yolo", AsYoloDetector)
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from newspaper_ocr.detectors.paddlex import PaddleXDetector
|
|
10
|
+
DETECTORS.register("paddlex", PaddleXDetector)
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|