pageledger 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.
- pageledger-0.1.0/CHANGELOG.md +93 -0
- pageledger-0.1.0/CITATION.cff +19 -0
- pageledger-0.1.0/LICENSE +21 -0
- pageledger-0.1.0/MANIFEST.in +19 -0
- pageledger-0.1.0/PKG-INFO +500 -0
- pageledger-0.1.0/README.md +463 -0
- pageledger-0.1.0/docs/adapter-protocol.md +229 -0
- pageledger-0.1.0/docs/audit-spec.md +56 -0
- pageledger-0.1.0/docs/comparison.md +101 -0
- pageledger-0.1.0/docs/design.md +180 -0
- pageledger-0.1.0/docs/examples/page-taxonomy.yml +27 -0
- pageledger-0.1.0/docs/examples/pageledger.yml +86 -0
- pageledger-0.1.0/docs/examples/run-policy.yml +65 -0
- pageledger-0.1.0/docs/examples/table-schema.yml +47 -0
- pageledger-0.1.0/docs/ocr-options.md +77 -0
- pageledger-0.1.0/docs/pdf-ocr-first-run.md +169 -0
- pageledger-0.1.0/docs/provenance-spec.md +144 -0
- pageledger-0.1.0/docs/reports/2026-06-24-pdf-ocr-readiness-final.md +148 -0
- pageledger-0.1.0/docs/reports/2026-06-24-pdf-ocr-stress.md +137 -0
- pageledger-0.1.0/docs/rerun-manifest-spec.md +120 -0
- pageledger-0.1.0/docs/route-map-spec.md +99 -0
- pageledger-0.1.0/docs/run-manifest-spec.md +218 -0
- pageledger-0.1.0/examples/cloud_vlm_adapter_skeleton.py +50 -0
- pageledger-0.1.0/examples/ocrmypdf_preprocess.sh +7 -0
- pageledger-0.1.0/examples/tesseract_pdftoppm_adapter.py +100 -0
- pageledger-0.1.0/pageledger/__init__.py +19 -0
- pageledger-0.1.0/pageledger/__main__.py +5 -0
- pageledger-0.1.0/pageledger/adapters.py +338 -0
- pageledger-0.1.0/pageledger/artifacts.py +234 -0
- pageledger-0.1.0/pageledger/cli.py +305 -0
- pageledger-0.1.0/pageledger/compare.py +169 -0
- pageledger-0.1.0/pageledger/config.py +299 -0
- pageledger-0.1.0/pageledger/doctor.py +112 -0
- pageledger-0.1.0/pageledger/runner.py +1226 -0
- pageledger-0.1.0/pageledger.egg-info/PKG-INFO +500 -0
- pageledger-0.1.0/pageledger.egg-info/SOURCES.txt +47 -0
- pageledger-0.1.0/pageledger.egg-info/dependency_links.txt +1 -0
- pageledger-0.1.0/pageledger.egg-info/entry_points.txt +2 -0
- pageledger-0.1.0/pageledger.egg-info/requires.txt +10 -0
- pageledger-0.1.0/pageledger.egg-info/top_level.txt +1 -0
- pageledger-0.1.0/pyproject.toml +68 -0
- pageledger-0.1.0/schemas/audit.schema.json +47 -0
- pageledger-0.1.0/schemas/cost.schema.json +67 -0
- pageledger-0.1.0/schemas/manifest.schema.json +151 -0
- pageledger-0.1.0/schemas/provenance-line.schema.json +91 -0
- pageledger-0.1.0/schemas/quality-line.schema.json +44 -0
- pageledger-0.1.0/schemas/run-log-line.schema.json +46 -0
- pageledger-0.1.0/setup.cfg +4 -0
- pageledger-0.1.0/skills/pageledger/SKILL.md +100 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to PageLedger will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to the artifact compatibility policy documented in
|
|
7
|
+
`docs/run-manifest-spec.md` → Compatibility Policy.
|
|
8
|
+
|
|
9
|
+
## 0.1.0 — 2026-07-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pageledger run` — extraction command supporting dry-run artifact
|
|
14
|
+
generation and built-in `text` and `pdf_text` adapter execution.
|
|
15
|
+
- `pageledger rerun` — re-extracts exactly the pages listed in a previous
|
|
16
|
+
run's `rerun-manifest.yml`, preserving page ids, recording parent lineage,
|
|
17
|
+
enforcing `run.max_rerun_depth`, and warning when a source checksum no
|
|
18
|
+
longer matches the parent manifest.
|
|
19
|
+
- `pageledger compare-runs` — page-by-page diff of two run directories:
|
|
20
|
+
character/word deltas, warnings resolved/introduced, adapters, cost.
|
|
21
|
+
- `pageledger init-config` — generates a minimal valid config to stdout or file.
|
|
22
|
+
- `pageledger inspect-run` — summarizes a completed or failed run directory.
|
|
23
|
+
- `pageledger doctor` — reports optional dependencies, external tool versions,
|
|
24
|
+
and redacted cloud environment status.
|
|
25
|
+
- Filesystem-native run artifacts: `manifest.json`, `route-map.yml`,
|
|
26
|
+
`config-snapshot.yml`, `audit.json`, `audit.md`, `provenance.jsonl`,
|
|
27
|
+
`quality.jsonl`, `cost.json`, `run.log`, `rerun-manifest.yml`, and per-page
|
|
28
|
+
output under `raw/`.
|
|
29
|
+
- Page-denominated budget enforcement — caps on pages, tokens, and dollars
|
|
30
|
+
with configurable warning thresholds, enforced preflight and mid-run.
|
|
31
|
+
- Cost provenance: `cost.json` reports `cost_basis` (`adapter_reported`,
|
|
32
|
+
`configured_rate`, `mixed`, `none`) and runner-measured
|
|
33
|
+
`extraction_seconds`; provenance lines carry per-page `extraction_seconds`.
|
|
34
|
+
- Retry with configurable `max_retries` and optional exponential backoff
|
|
35
|
+
(`retry.backoff: exponential`, 0.5 s base doubling to an 8 s cap).
|
|
36
|
+
- Quality signal diagnostics with six-item warning taxonomy (`empty_text`,
|
|
37
|
+
`short_text`, `replacement_characters`, `control_characters`,
|
|
38
|
+
`suspicious_symbol_density`, `suspicious_embedded_text_delta`).
|
|
39
|
+
- Quality-warning pages automatically routed to audit `review_queue` with
|
|
40
|
+
`reason: "quality_warning"`.
|
|
41
|
+
- `quality_warning_pages` rollup count in `manifest.summary`.
|
|
42
|
+
- Custom adapters via `module.path:object` import strings.
|
|
43
|
+
- Adapter conformance checker: `pageledger.adapters.adapter_conformance_check()`.
|
|
44
|
+
- Adapter metadata validation at load time (name, version, deterministic,
|
|
45
|
+
input_types, output_types, capabilities, supports, extract).
|
|
46
|
+
- `usage.pages == 1` enforcement — each `extract()` call handles exactly one page.
|
|
47
|
+
- Executable rerun manifests: `rerun_status`
|
|
48
|
+
(`executable`/`empty_queue`/`no_further_generations`), `rerun_depth`
|
|
49
|
+
generation tracking, and the `max_rerun_depth == 0` guard producing empty
|
|
50
|
+
items.
|
|
51
|
+
- Config validation with key-path error messages and suspicious-config warnings
|
|
52
|
+
(empty taxonomy, unknown top-level keys, impossible budget thresholds).
|
|
53
|
+
- JSON Schema files for all JSON/JSONL artifacts under `schemas/`.
|
|
54
|
+
- Schema validation tests covering dry-run, execute, budget failure, adapter
|
|
55
|
+
failure, and empty-review-queue scenarios.
|
|
56
|
+
- Failure recovery: partial-run guarantees documented with scenario table,
|
|
57
|
+
write-order, and common error/user-action mappings.
|
|
58
|
+
- Comprehensive test suite: 197 tests covering all CLI commands, adapters,
|
|
59
|
+
quality signals, rerun execution, cross-run comparison, cost provenance,
|
|
60
|
+
schemas, failure paths, and edge-case inputs.
|
|
61
|
+
- CI workflow with test matrix (with/without PDF extra, Python 3.10–3.13)
|
|
62
|
+
and wheel/sdist smoke tests.
|
|
63
|
+
- Release checklist under `.planning/release-checklist.md`.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- "Core Modules" → "Design Architecture" with explicit implementation-status
|
|
68
|
+
labels in README.
|
|
69
|
+
- Added "Current Runtime Capabilities" (three tiers) and "Known Limits" sections.
|
|
70
|
+
- Claim audit: removed optimistic score language, constrained the public promise
|
|
71
|
+
to implemented behavior only.
|
|
72
|
+
- `adapter-protocol.md` rewritten with frozen contract, `usage.pages == 1` rule,
|
|
73
|
+
and subprocess/timeout guidance.
|
|
74
|
+
|
|
75
|
+
### Removed
|
|
76
|
+
|
|
77
|
+
- Python 3.14 classifier (not yet released).
|
|
78
|
+
|
|
79
|
+
### Performance
|
|
80
|
+
|
|
81
|
+
- Tested scale: 5,000 text pages in 2.4s (~2,100 pages/sec) locally.
|
|
82
|
+
Artifact counts verified: raw files == provenance lines == quality lines
|
|
83
|
+
== pages extracted. Stress tests reproducible without credentials or
|
|
84
|
+
private data.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- `--json` now emits parseable error JSON (`{"status": "error", "error": "..."}`)
|
|
89
|
+
on failure instead of unstructured stderr-only errors.
|
|
90
|
+
- Unreadable input files now produce clean `RuntimeError` instead of raw
|
|
91
|
+
`PermissionError` tracebacks.
|
|
92
|
+
- Rerun manifest `max_rerun_depth == 0` now correctly produces empty `items: []`
|
|
93
|
+
with `rerun_status: "no_further_generations"`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
title: "PageLedger: Auditable OCR/VLM Extraction Workflows"
|
|
3
|
+
message: "If you use PageLedger in research, please cite the software and separately cite the source dataset or extraction run you created with it."
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Busscher
|
|
7
|
+
given-names: Peter
|
|
8
|
+
abstract: "PageLedger is an alpha toolkit for routing, auditing, budgeting, and preserving filesystem-native provenance around OCR/VLM document extraction workflows."
|
|
9
|
+
keywords:
|
|
10
|
+
- document extraction
|
|
11
|
+
- OCR
|
|
12
|
+
- vision-language models
|
|
13
|
+
- digital humanities
|
|
14
|
+
- provenance
|
|
15
|
+
- corpus infrastructure
|
|
16
|
+
license: MIT
|
|
17
|
+
repository-code: "https://github.com/peterbussch/pageledger"
|
|
18
|
+
version: "0.1.0"
|
|
19
|
+
date-released: "2026-07-06"
|
pageledger-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Busscher
|
|
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,19 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CITATION.cff
|
|
5
|
+
recursive-include docs *.md *.yml
|
|
6
|
+
recursive-include examples *.py *.sh
|
|
7
|
+
recursive-include schemas *.json
|
|
8
|
+
recursive-include skills *.md
|
|
9
|
+
|
|
10
|
+
# Local-only material that may exist in a development working tree.
|
|
11
|
+
# Never let it into the sdist even when present on disk.
|
|
12
|
+
exclude docs/autoresearch.md
|
|
13
|
+
exclude _session_research.md
|
|
14
|
+
recursive-exclude prompts *
|
|
15
|
+
recursive-exclude assets *
|
|
16
|
+
prune .planning
|
|
17
|
+
prune .stress
|
|
18
|
+
prune .hermes
|
|
19
|
+
prune runs
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pageledger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Page-denominated document extraction run ledger
|
|
5
|
+
Author: Peter Busscher
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/peterbussch/pageledger
|
|
8
|
+
Project-URL: Documentation, https://github.com/peterbussch/pageledger#readme
|
|
9
|
+
Project-URL: PDF/OCR First Run, https://github.com/peterbussch/pageledger/blob/main/docs/pdf-ocr-first-run.md
|
|
10
|
+
Project-URL: OCR Options, https://github.com/peterbussch/pageledger/blob/main/docs/ocr-options.md
|
|
11
|
+
Project-URL: Issues, https://github.com/peterbussch/pageledger/issues
|
|
12
|
+
Project-URL: Source, https://github.com/peterbussch/pageledger
|
|
13
|
+
Keywords: document-ai,ocr,vlm,digital-humanities,provenance
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Classifier: Topic :: Text Processing
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: PyYAML>=6.0.2
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
31
|
+
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
33
|
+
Requires-Dist: twine>=6.0; extra == "dev"
|
|
34
|
+
Provides-Extra: pdf
|
|
35
|
+
Requires-Dist: pypdf>=6; extra == "pdf"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<picture>
|
|
40
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/peterbussch/pageledger/main/assets/pageledger-lockup-horizontal-reversed.png">
|
|
41
|
+
<img src="https://raw.githubusercontent.com/peterbussch/pageledger/main/assets/pageledger-lockup-horizontal.png" alt="PageLedger — the tallied page" width="440">
|
|
42
|
+
</picture>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<p align="center"><em>Record, route, and review document extraction — one page at a time.</em></p>
|
|
46
|
+
|
|
47
|
+
**PageLedger is for researchers who extract text from archives at scale** —
|
|
48
|
+
digital humanities labs, libraries, historians, and social scientists running
|
|
49
|
+
OCR/VLM over thousands of scanned pages who must be able to answer, months
|
|
50
|
+
later: which pages went through which engine and prompt, what it cost, which
|
|
51
|
+
pages are suspect, and what happened when they were re-extracted.
|
|
52
|
+
|
|
53
|
+
It is a page-denominated **run ledger** — not an OCR model, not a
|
|
54
|
+
PDF-to-Markdown converter. You bring the extraction engine (Tesseract,
|
|
55
|
+
Docling, Marker, a cloud VLM); PageLedger routes pages to it, enforces
|
|
56
|
+
page/token/dollar budgets, and writes the evidence as plain files:
|
|
57
|
+
provenance, quality diagnostics, cost with its basis, audit queues, and
|
|
58
|
+
executable rerun plans. No service, no database — a run directory you can
|
|
59
|
+
grep, cite, and reproduce.
|
|
60
|
+
|
|
61
|
+
PageLedger grew out of
|
|
62
|
+
[soviet-corpus](https://github.com/peterbussch/soviet-corpus), a Soviet census
|
|
63
|
+
digitization and corpus-linguistics project where those questions had to be
|
|
64
|
+
answerable for source citation and methodological review. The run-ledger
|
|
65
|
+
layer was extracted from that working pipeline and rebuilt as a standalone,
|
|
66
|
+
domain-neutral tool.
|
|
67
|
+
|
|
68
|
+
`0.1.0` is an early alpha focused on the run-ledger core: built-in `text` and
|
|
69
|
+
`pdf_text` (born-digital) adapters, custom adapters via import strings,
|
|
70
|
+
dry runs, budgets, quality signals, page-scoped reruns
|
|
71
|
+
(`pageledger rerun`), and cross-run diffs (`pageledger compare-runs`). It
|
|
72
|
+
does not yet OCR images itself, call network VLMs out of the box,
|
|
73
|
+
automatically classify pages, align records to schemas, or compute audit
|
|
74
|
+
grades — see [Current Runtime Capabilities](#current-runtime-capabilities).
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install pageledger
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Optional born-digital PDF text extraction:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install "pageledger[pdf]"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Quickstart
|
|
89
|
+
|
|
90
|
+
Create a text fixture and a starter config:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
printf 'first page\fsecond page\n' > sample.txt
|
|
94
|
+
pageledger init-config --out pageledger.yml
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Inspect the run without extraction:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pageledger run sample.txt --config pageledger.yml --out runs/dry --dry-run
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run the built-in text adapter, then summarize the run directory:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pageledger run sample.txt --config pageledger.yml --out runs/text --json
|
|
107
|
+
pageledger inspect-run runs/text
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For a born-digital PDF, install the optional PDF extra and use `pdf_text`:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install "pageledger[pdf]"
|
|
114
|
+
cat > pageledger-pdf.yml <<'YAML'
|
|
115
|
+
schema_version: "0.1"
|
|
116
|
+
taxonomy:
|
|
117
|
+
page_types:
|
|
118
|
+
prose:
|
|
119
|
+
default_action: transcribe_text
|
|
120
|
+
run:
|
|
121
|
+
adapter: pdf_text
|
|
122
|
+
YAML
|
|
123
|
+
pageledger run document.pdf --config pageledger-pdf.yml --out runs/pdf-dry --dry-run
|
|
124
|
+
pageledger run document.pdf --config pageledger-pdf.yml --out runs/pdf-text --json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--out` must point to a new empty directory. For dry runs over PDFs, PageLedger
|
|
128
|
+
uses `pypdf` page counts when the PDF extra is installed; execution with
|
|
129
|
+
`pdf_text` extracts an existing text layer and does not OCR scanned images.
|
|
130
|
+
|
|
131
|
+
The run directory contains `manifest.json`, `route-map.yml`, `audit.json`,
|
|
132
|
+
`audit.md`, `provenance.jsonl`, `quality.jsonl`, `cost.json`, `run.log`,
|
|
133
|
+
`rerun-manifest.yml`, and per-page raw output under `raw/`.
|
|
134
|
+
|
|
135
|
+
For a real PDF/OCR walkthrough, see
|
|
136
|
+
[`docs/pdf-ocr-first-run.md`](docs/pdf-ocr-first-run.md). For local, free,
|
|
137
|
+
open-source, cloud, and hybrid extraction choices, see
|
|
138
|
+
[`docs/ocr-options.md`](docs/ocr-options.md). Custom adapter examples live
|
|
139
|
+
under [`examples/`](examples/), including Tesseract via `pdftoppm`, OCRmyPDF
|
|
140
|
+
preprocessing, and a redacted cloud/VLM skeleton.
|
|
141
|
+
|
|
142
|
+
## How a Run Works
|
|
143
|
+
|
|
144
|
+
```mermaid
|
|
145
|
+
flowchart TD
|
|
146
|
+
A["inputs (text / PDF)"] --> B["paginate<br/>(form-feed or PDF pages)"]
|
|
147
|
+
B --> C["route pages<br/>route-map.yml"]
|
|
148
|
+
C --> D{"budget preflight<br/>max_pages"}
|
|
149
|
+
D -- over cap --> X["refuse: nothing written"]
|
|
150
|
+
D -- ok --> E["extract page via adapter<br/>(retry + backoff)"]
|
|
151
|
+
E --> F["quality signals<br/>quality.jsonl"]
|
|
152
|
+
E --> G["provenance.jsonl<br/>+ cost.json (cost_basis)"]
|
|
153
|
+
E --> H{"budget mid-run<br/>pages / tokens / USD"}
|
|
154
|
+
H -- over cap --> Y["halt: manifest status=failed,<br/>partial artifacts consistent"]
|
|
155
|
+
H -- ok --> E
|
|
156
|
+
F -- warnings --> I["review queue<br/>audit.json / audit.md"]
|
|
157
|
+
I --> J["rerun-manifest.yml"]
|
|
158
|
+
J -- "pageledger rerun<br/>(stronger adapter)" --> E
|
|
159
|
+
G --> K["pageledger compare-runs<br/>(parent vs rerun)"]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Every box on the right is a plain file in the run directory; nothing needs a
|
|
163
|
+
service or database.
|
|
164
|
+
|
|
165
|
+
## Current Runtime Capabilities
|
|
166
|
+
|
|
167
|
+
**Built-in and tested:**
|
|
168
|
+
|
|
169
|
+
- `pageledger run` for text fixtures (form-feed pagination) and born-digital PDF
|
|
170
|
+
text layers (via `pageledger[pdf]`).
|
|
171
|
+
- Dry-run mode that generates full run artifacts without calling extractors.
|
|
172
|
+
- Per-page provenance (`provenance.jsonl`), quality diagnostics
|
|
173
|
+
(`quality.jsonl`), cost rollups (`cost.json`), structured run logs
|
|
174
|
+
(`run.log`), and audit/review queues (`audit.json`, `audit.md`).
|
|
175
|
+
- Page-denominated budget enforcement (pages, tokens, dollars) with preflight
|
|
176
|
+
and per-page caps.
|
|
177
|
+
- Retry with configurable `max_retries` and optional exponential backoff
|
|
178
|
+
(`retry.backoff: exponential`).
|
|
179
|
+
- `pageledger rerun` — re-extracts exactly the pages listed in a previous
|
|
180
|
+
run's rerun manifest (typically with a stronger adapter), preserving page
|
|
181
|
+
ids, recording parent lineage, enforcing `max_rerun_depth`, and warning if
|
|
182
|
+
a source file changed since the parent run.
|
|
183
|
+
- `pageledger compare-runs` — page-by-page diff of two runs: character/word
|
|
184
|
+
deltas, warnings resolved or introduced, adapters, and cost.
|
|
185
|
+
- Cost provenance: `cost.json` records `cost_basis` (`adapter_reported`,
|
|
186
|
+
`configured_rate`, `mixed`, or `none`) so derived accounting rates are
|
|
187
|
+
never mistaken for provider-billed spend, plus runner-measured
|
|
188
|
+
`extraction_seconds` per page and in total.
|
|
189
|
+
- `pageledger doctor` diagnostics for optional PDF/OCR/cloud tooling.
|
|
190
|
+
- Configurable custom adapters via `module.path:object` import strings.
|
|
191
|
+
|
|
192
|
+
**Adapter-supported but user-supplied:**
|
|
193
|
+
|
|
194
|
+
- OCR via Tesseract (external `pdftoppm` + `tesseract` commands), OCRmyPDF
|
|
195
|
+
preprocessing, or any external engine wrapped as a custom adapter.
|
|
196
|
+
- Cloud OCR/VLM adapters (user provides API keys, adapter code, and pricing).
|
|
197
|
+
- Local document-conversion engines (Docling, Marker, Surya) through custom
|
|
198
|
+
adapters.
|
|
199
|
+
- PDF page counting for custom adapters that expose `page_count(source)`.
|
|
200
|
+
|
|
201
|
+
**Documented future work (not yet implemented):**
|
|
202
|
+
|
|
203
|
+
- Automatic page classification (the alpha routes every page to `review` in
|
|
204
|
+
dry-run mode; no classifier ships).
|
|
205
|
+
- Schema alignment execution (the schema config section is parsed but the
|
|
206
|
+
aligner does not yet produce normalized records).
|
|
207
|
+
- Audit grading (review queues are populated but no grades are computed).
|
|
208
|
+
- Multi-adapter routing chains, `rerun_if`/`quarantine_if` policy enforcement,
|
|
209
|
+
and staged CLI commands (`classify`, `extract`, `align`, `audit`).
|
|
210
|
+
|
|
211
|
+
## Known Limits
|
|
212
|
+
|
|
213
|
+
- **`pdf_text` reads existing text layers.** It does not perform OCR. Scanned
|
|
214
|
+
PDFs, image-heavy PDFs, or PDFs with noisy/absent text layers need an external
|
|
215
|
+
OCR step (OCRmyPDF, Tesseract, Docling, Marker, Surya, or cloud OCR) before
|
|
216
|
+
PageLedger can record useful text.
|
|
217
|
+
- **Quality signals are diagnostic, not calibrated.** `quality.jsonl` records
|
|
218
|
+
per-page evidence: character/word counts, replacement characters, control
|
|
219
|
+
characters, suspicious symbol density, and embedded-text deltas. A six-item
|
|
220
|
+
warning taxonomy (empty_text, short_text, replacement_characters,
|
|
221
|
+
control_characters, suspicious_symbol_density, suspicious_embedded_text_delta)
|
|
222
|
+
flags pages for review. These signals are NOT OCR accuracy scores — they are
|
|
223
|
+
evidence a human should weigh.
|
|
224
|
+
- **Quality-warning pages appear in the audit review queue.** For execution
|
|
225
|
+
runs, every page with one or more quality warnings is added to
|
|
226
|
+
`audit.json` → `review_queue` with reason `"quality_warning"`. Dry-run
|
|
227
|
+
review entries use route-based reasons (`"no_classifier_available"`).
|
|
228
|
+
- **No automatic page routing.** Every page routes to the configured
|
|
229
|
+
`default_action` (or `review` in dry-run mode). Projects that need
|
|
230
|
+
page-type-aware routing must classify pages outside PageLedger or wait for
|
|
231
|
+
a future classifier.
|
|
232
|
+
- **Schema alignment does not execute.** The schema config section is validated
|
|
233
|
+
and preserved in the config snapshot, but the runner does not yet align
|
|
234
|
+
extractor output to declared columns, types, or checks.
|
|
235
|
+
- **Reruns re-extract listed pages; they do not merge results.**
|
|
236
|
+
`rerun-manifest.yml` lists pages from `audit.json` → `review_queue`;
|
|
237
|
+
`pageledger rerun` re-extracts exactly those pages into a new run directory
|
|
238
|
+
with parent lineage. `rerun_status` is `"executable"`, `"empty_queue"`, or
|
|
239
|
+
`"no_further_generations"` (depth cap reached). Combining parent and rerun
|
|
240
|
+
outputs into one corpus remains the project's decision —
|
|
241
|
+
`pageledger compare-runs` shows the per-page evidence for making it.
|
|
242
|
+
|
|
243
|
+
## Tested Scale
|
|
244
|
+
|
|
245
|
+
The 0.1.0 alpha has been tested locally on:
|
|
246
|
+
|
|
247
|
+
- **5,000 synthetic text pages** (2.4s, ~2,100 pages/sec, artifact counts
|
|
248
|
+
verified — raw files = provenance lines = quality lines = pages extracted).
|
|
249
|
+
- **1,000 synthetic text pages** (0.25s, ~4,000 pages/sec).
|
|
250
|
+
- **72-page born-digital PDF** (via `pageledger[pdf]`).
|
|
251
|
+
- **5 small checked-in text fixtures** covering clean, multipage, OCR-noisy,
|
|
252
|
+
blank, and short text.
|
|
253
|
+
|
|
254
|
+
This is evidence of the envelope, not a universal performance guarantee.
|
|
255
|
+
Stress tests are marked `@pytest.mark.stress` and skipped in default CI.
|
|
256
|
+
Run them with:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
python -m pytest tests/pageledger/ -m stress
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## The Canonical Unit: Pages
|
|
263
|
+
|
|
264
|
+
PageLedger's defining decision is that **the page is the canonical unit of
|
|
265
|
+
work.** It is the only unit every extraction backend shares:
|
|
266
|
+
|
|
267
|
+
- Cloud OCR (Textract, Azure Document Intelligence, Google Document AI, Mistral
|
|
268
|
+
OCR) bills and reports **per page**.
|
|
269
|
+
- VLM/LLM extractors expose **tokens** — but only on model-backed paths; tokens
|
|
270
|
+
are meaningless for classical OCR.
|
|
271
|
+
- Self-hosted engines (e.g. Docling) have **no dollar cost at all**, only compute
|
|
272
|
+
time.
|
|
273
|
+
|
|
274
|
+
Because pages are the common denominator, denominating routing, budgeting, and
|
|
275
|
+
audit in pages is what lets you **compare, budget, and reproduce a run across
|
|
276
|
+
heterogeneous providers** — a Textract run against a Mistral run against a local
|
|
277
|
+
model — with one number. Tokens, compute-seconds, and dollars are all carried as
|
|
278
|
+
*optional, provider-conditional* signals on top.
|
|
279
|
+
|
|
280
|
+
Every adapter reports a usage record where **`pages` is required** and
|
|
281
|
+
everything else is optional:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
usage = {
|
|
285
|
+
"pages": 1, # REQUIRED — the portable unit
|
|
286
|
+
"tokens": None, # VLM/LLM paths only
|
|
287
|
+
"compute_seconds": None, # self-hosted engines
|
|
288
|
+
"cost_usd": None, # optional adapter-reported passthrough
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Dollar cost is **derived by PageLedger, never required of the adapter**, in
|
|
293
|
+
priority order: (1) adapter-reported `cost_usd`, (2) configured unit rates
|
|
294
|
+
(`cost_per_page` / `cost_per_1k_tokens`), (3) otherwise `null` — the run still
|
|
295
|
+
reports raw page counts. Budgets cap on **pages, tokens, or dollars**, whichever
|
|
296
|
+
the config sets, because the page count is the only value always present.
|
|
297
|
+
|
|
298
|
+
## Why Not Just Use OCR?
|
|
299
|
+
|
|
300
|
+
OCR and document-AI tools are improving quickly. Mistral OCR, Google Document
|
|
301
|
+
AI, Azure Document Intelligence, AWS Bedrock Data Automation, Docling, Marker,
|
|
302
|
+
Surya, olmOCR, OCR-D, and Unstructured all cover parts of extraction,
|
|
303
|
+
conversion, layout analysis, validation, or workflow orchestration.
|
|
304
|
+
|
|
305
|
+
That makes PageLedger's scope deliberately narrow: it is useful when you need a
|
|
306
|
+
local, provider-neutral ledger around those tools, not when you only need one
|
|
307
|
+
tool to convert a document once.
|
|
308
|
+
|
|
309
|
+
| Tool | Strong At | PageLedger Difference |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| Mistral OCR | Hosted OCR/document understanding with Markdown and table reconstruction. | PageLedger records why pages went to Mistral, what it cost, and which pages need review or rerun. |
|
|
312
|
+
| Google Document AI / Gemini layout parser | Managed OCR, layout parsing, table structure, and RAG-oriented chunking. | PageLedger keeps provider-neutral run artifacts and project-local audit queues outside a cloud workflow. |
|
|
313
|
+
| Azure Document Intelligence | Managed text, key-value, table, and field extraction. | PageLedger can track Azure runs beside local or other-provider runs with the same page-denominated manifest. |
|
|
314
|
+
| AWS Bedrock Data Automation | Managed classification, extraction, validation, HITL review, and business-rule workflows. | PageLedger stays lighter and filesystem-native for research projects that do not want a cloud stack. |
|
|
315
|
+
| Docling | Converting PDFs and documents into structured output. | PageLedger records which pages went to Docling, the usage and cost evidence, and which pages need review or rerun. |
|
|
316
|
+
| Marker | High-quality PDF-to-Markdown conversion. | PageLedger handles routing, run manifests, cost controls, and review queues around conversion. |
|
|
317
|
+
| Surya | OCR, layout analysis, table recognition. | PageLedger can call Surya as an extractor, then audit the result across a full archive run. |
|
|
318
|
+
| olmOCR | LLM-based PDF extraction with document-oriented output. | PageLedger adds page routing, quality diagnostics, cost evidence, and rerun manifests. |
|
|
319
|
+
| OCR-D | Mature OCR workflow model with METS/PAGE/ALTO conventions. | PageLedger aims to be lighter, VLM-aware, and friendlier to local research workflows that need JSON/YAML artifacts before full library infrastructure. |
|
|
320
|
+
| Unstructured | Document partitioning and preprocessing for downstream use. | PageLedger focuses on extraction run control: what ran, what passed, what failed, what cost money, and what should be rerun. |
|
|
321
|
+
|
|
322
|
+
Non-goal: PageLedger is not an OCR engine, PDF converter, or layout detector.
|
|
323
|
+
If a project already has clean structured text from a single tool and does not
|
|
324
|
+
need routing, schema checks, review queues, cost tracking, or reruns, it likely
|
|
325
|
+
does not need PageLedger.
|
|
326
|
+
|
|
327
|
+
For practical provider-agnostic choices, see
|
|
328
|
+
[`docs/ocr-options.md`](docs/ocr-options.md). The recommended workflow: run cheap
|
|
329
|
+
local extraction first, inspect `quality.jsonl` warnings to identify weak pages,
|
|
330
|
+
re-extract just those pages with a stronger adapter via
|
|
331
|
+
`pageledger rerun runs/first/ --config stronger.yml --out runs/second/`, then
|
|
332
|
+
`pageledger compare-runs runs/first/ runs/second/` to see what improved.
|
|
333
|
+
|
|
334
|
+
## What PageLedger Cannot Do
|
|
335
|
+
|
|
336
|
+
PageLedger does not make OCR or VLM output correct by itself. It cannot
|
|
337
|
+
calibrate confidence scores across unrelated extractors, guarantee text
|
|
338
|
+
accuracy, infer a project's source citation, or make a right-to-left,
|
|
339
|
+
mixed-script, tabular, or handwritten collection work without explicit adapter
|
|
340
|
+
and schema configuration.
|
|
341
|
+
|
|
342
|
+
Its job is narrower: preserve enough evidence that a researcher can see what
|
|
343
|
+
ran, what failed, what is uncertain, and what should be reviewed or rerun.
|
|
344
|
+
|
|
345
|
+
## Why This Exists
|
|
346
|
+
|
|
347
|
+
Modern OCR and document-AI tools can produce impressive text, tables, and
|
|
348
|
+
Markdown, but real archive work fails in smaller and more frustrating ways:
|
|
349
|
+
|
|
350
|
+
- Some pages are title pages, indexes, maps, appendices, blanks, or prose,
|
|
351
|
+
not data pages.
|
|
352
|
+
- Expensive VLM calls should be routed deliberately instead of sprayed across
|
|
353
|
+
every page.
|
|
354
|
+
- Model-emitted tables often have variant headers, shifted columns, missing
|
|
355
|
+
rows, merged cells, or inconsistent numeric formats.
|
|
356
|
+
- Confidence scores are weak unless they are checked against schema,
|
|
357
|
+
arithmetic, page totals, and model agreement.
|
|
358
|
+
- Long extraction runs need cost tracking, retry logic, checkpoints, and
|
|
359
|
+
provenance that survives reruns.
|
|
360
|
+
|
|
361
|
+
PageLedger is for projects where "the model returned JSON" is only the
|
|
362
|
+
beginning of the work. The current alpha is for proving local run evidence and
|
|
363
|
+
adapter contracts before trusting larger extraction workflows.
|
|
364
|
+
|
|
365
|
+
## Audience
|
|
366
|
+
|
|
367
|
+
- Digital humanities labs processing archival scans.
|
|
368
|
+
- Libraries and archives building reproducible extraction pipelines.
|
|
369
|
+
- Historians and social scientists extracting structured data from source
|
|
370
|
+
books, statistical tables, reports, and registers.
|
|
371
|
+
- Civic, legal, nonprofit, and local-government document projects.
|
|
372
|
+
- Document-AI engineers who need a lightweight local control plane around
|
|
373
|
+
Docling, Marker, Surya, olmOCR, Tesseract, or API VLMs.
|
|
374
|
+
|
|
375
|
+
## Architecture and Roadmap
|
|
376
|
+
|
|
377
|
+
The alpha implements the **run controller** (budgets, retry, provenance,
|
|
378
|
+
quality signals, audit queues, rerun execution, cross-run comparison) and
|
|
379
|
+
the **adapter protocol** (thin wrappers that accept a routed page and return
|
|
380
|
+
serializable content, warnings, confidence hints, and usage metadata — see
|
|
381
|
+
[`docs/adapter-protocol.md`](docs/adapter-protocol.md)).
|
|
382
|
+
|
|
383
|
+
Automatic page routing, schema alignment, audit grading, rerun execution, and
|
|
384
|
+
staged CLI commands are design targets, documented with examples in
|
|
385
|
+
[`docs/design.md`](docs/design.md).
|
|
386
|
+
|
|
387
|
+
## CLI
|
|
388
|
+
|
|
389
|
+
Six commands ship in the alpha; `run` and `rerun` are the ones that extract:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
pageledger run scans/ --config pageledger.yml --out runs/run-001/
|
|
393
|
+
pageledger run scans/ --config pageledger.yml --out runs/run-001/ --dry-run
|
|
394
|
+
pageledger run scans/ --config pageledger.yml --out runs/run-001/ --json --log-level INFO
|
|
395
|
+
pageledger rerun runs/run-001/ --config stronger.yml --out runs/run-002/
|
|
396
|
+
# re-extract only the flagged pages
|
|
397
|
+
pageledger compare-runs runs/run-001/ runs/run-002/
|
|
398
|
+
# page-by-page diff of two runs
|
|
399
|
+
pageledger init-config --out pageledger.yml # write a minimal valid config
|
|
400
|
+
pageledger inspect-run runs/run-001/ # summarize a run directory
|
|
401
|
+
pageledger doctor --json # check optional dependencies
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
`--dry-run` generates the route map and planning artifacts without calling
|
|
405
|
+
extractors, so you can inspect routing before spending money or trusting
|
|
406
|
+
output. `--json` makes command status scriptable (parseable stdout, errors on
|
|
407
|
+
stderr), and `--log-level` controls the detail written to the run log.
|
|
408
|
+
|
|
409
|
+
Current non-dry-run support is deliberately tiny. Set `run.adapter: text` to
|
|
410
|
+
write each page of a UTF-8 text input to `raw/doc_0001_page_0001.txt` (text
|
|
411
|
+
sources are split into pages on the form-feed character), or install
|
|
412
|
+
`pageledger[pdf]` and set `run.adapter: pdf_text` for born-digital PDF text
|
|
413
|
+
extraction through `pypdf`. Both paths emit one `provenance.jsonl` line per
|
|
414
|
+
page. This proves the preflight paginate → route → extract → provenance path
|
|
415
|
+
before OCR/VLM integrations. For scanned PDFs or richer layout extraction, run
|
|
416
|
+
an OCR/conversion tool first or point `run.adapter` at a project adapter such as
|
|
417
|
+
`my_project.adapters:TesseractCliAdapter`; PageLedger will keep the same
|
|
418
|
+
manifest, cost, log, and provenance envelope around that external engine.
|
|
419
|
+
|
|
420
|
+
The doctor output reports optional packages, common OCR commands, and whether
|
|
421
|
+
cloud OCR/VLM keys are present without printing their values.
|
|
422
|
+
|
|
423
|
+
Starter config examples live in `docs/examples/`:
|
|
424
|
+
|
|
425
|
+
- `pageledger.yml` (recommended first-use combined config)
|
|
426
|
+
- `page-taxonomy.yml`
|
|
427
|
+
- `table-schema.yml`
|
|
428
|
+
- `run-policy.yml`
|
|
429
|
+
|
|
430
|
+
The split files are useful for documentation and larger projects. The
|
|
431
|
+
recommended v0.1 user experience should start with one `pageledger.yml` that
|
|
432
|
+
contains `taxonomy`, `schema`, and `run` sections.
|
|
433
|
+
|
|
434
|
+
## Run Artifacts
|
|
435
|
+
|
|
436
|
+
A run should be inspectable without a database or service:
|
|
437
|
+
|
|
438
|
+
```text
|
|
439
|
+
runs/run-001/
|
|
440
|
+
├── manifest.json
|
|
441
|
+
├── config-snapshot.yml
|
|
442
|
+
├── route-map.yml
|
|
443
|
+
├── raw/
|
|
444
|
+
│ └── doc_0001_page_0002.txt
|
|
445
|
+
├── normalized/
|
|
446
|
+
├── audit.json
|
|
447
|
+
├── audit.md
|
|
448
|
+
├── provenance.jsonl
|
|
449
|
+
├── quality.jsonl
|
|
450
|
+
├── cost.json
|
|
451
|
+
├── run.log
|
|
452
|
+
└── rerun-manifest.yml
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
The important artifact is not only the extracted data, but the evidence around
|
|
456
|
+
it: which pages were skipped, which model or engine was used, what usage and
|
|
457
|
+
cost evidence was observed, which pages failed or need review, and what should
|
|
458
|
+
be rerun or reviewed.
|
|
459
|
+
|
|
460
|
+
`manifest.json` is the canonical run artifact. `audit.md` is a human rendering
|
|
461
|
+
of `audit.json`, not a second independent source of truth. Quarantined pages
|
|
462
|
+
live in `audit.json` under `quarantine_queue`. `config-snapshot.yml` preserves
|
|
463
|
+
the exact user config that produced the run. `run.log` is JSONL — one line per
|
|
464
|
+
extractor call with timestamp, `page_id`, adapter, status, and any error — so
|
|
465
|
+
partial or failed runs stay greppable. Every artifact validates against the
|
|
466
|
+
JSON Schemas in [`schemas/`](schemas/). The artifact directory keeps the name
|
|
467
|
+
`normalized/` for stability, but the current alpha does not write normalized
|
|
468
|
+
records yet; schema alignment remains a documented design target.
|
|
469
|
+
|
|
470
|
+
## Design Principles
|
|
471
|
+
|
|
472
|
+
- Record uncertainty; do not silently fix it.
|
|
473
|
+
- Treat heuristic confidence as evidence, not probability. Uncalibrated
|
|
474
|
+
extractors should not imply certainty.
|
|
475
|
+
- Every run produces inspectable artifacts on disk.
|
|
476
|
+
- Adapters are thin: PageLedger does not own extraction, it owns the process
|
|
477
|
+
around extraction.
|
|
478
|
+
- The manifest, route map, and provenance files should be useful without a
|
|
479
|
+
running service or database.
|
|
480
|
+
- Preserve separate citations for software and source data.
|
|
481
|
+
|
|
482
|
+
## Related Documents
|
|
483
|
+
|
|
484
|
+
- [`schemas/`](schemas/) — JSON Schema files, the machine-readable authority
|
|
485
|
+
for artifact contracts
|
|
486
|
+
- [`docs/design.md`](docs/design.md) — design targets: router, schema aligner,
|
|
487
|
+
rerun policies, staged CLI, open research questions
|
|
488
|
+
- [`docs/comparison.md`](docs/comparison.md) — positioning against the 2026
|
|
489
|
+
extraction ecosystem
|
|
490
|
+
- [`docs/run-manifest-spec.md`](docs/run-manifest-spec.md),
|
|
491
|
+
[`docs/route-map-spec.md`](docs/route-map-spec.md),
|
|
492
|
+
[`docs/audit-spec.md`](docs/audit-spec.md),
|
|
493
|
+
[`docs/provenance-spec.md`](docs/provenance-spec.md),
|
|
494
|
+
[`docs/rerun-manifest-spec.md`](docs/rerun-manifest-spec.md) — per-artifact
|
|
495
|
+
field specs
|
|
496
|
+
- [`docs/adapter-protocol.md`](docs/adapter-protocol.md) — how to wrap an
|
|
497
|
+
OCR/VLM tool as an adapter
|
|
498
|
+
- [`AGENTS.md`](AGENTS.md) — orientation for AI coding agents working in this
|
|
499
|
+
repository
|
|
500
|
+
- [`CITATION.cff`](CITATION.cff) — how to cite PageLedger in research output
|