1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = "==3.13.*"
# dependencies = []
# ///
from __future__ import annotations
import argparse
import os
import subprocess
import sys
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run the noo-harvest paper extraction and phase-2 verification pipeline."
)
parser.add_argument("input_pdf")
parser.add_argument("output_dir")
parser.add_argument("--model-path", help="Explicit local DeepSeek-OCR-2 snapshot for paper_ocr.py")
parser.add_argument("--ocr-python", help="Explicit Python interpreter for the OCR runtime")
parser.add_argument("--pages", help="Comma-separated 1-based pages and ranges, e.g. 1,3-5")
parser.add_argument(
"--extract-only",
action="store_true",
help="Run OCR extraction only; do not invoke phase-2 verification",
)
parser.add_argument(
"--verify-only",
action="store_true",
help="Run phase-2 verification from an existing extracted/document.blocks.json",
)
parser.add_argument(
"--force-extract",
action="store_true",
help="Rerun OCR extraction even if extracted/document.blocks.json already exists",
)
return parser.parse_args()
def run_step(argv: list[str], *, cwd: Path | None = None) -> None:
subprocess.run(argv, check=True, cwd=cwd)
def discover_ocr_python(explicit: str | None) -> str:
candidates: list[Path] = []
if explicit:
candidates.append(Path(explicit).expanduser())
env_python = os.environ.get("NOO_HARVEST_OCR_PYTHON")
if env_python:
candidates.append(Path(env_python).expanduser())
repo_root = Path(__file__).resolve().parents[2]
candidates.append(repo_root / ".tmp/deepseek-ocr2/.venv/bin/python")
for candidate in candidates:
if candidate.is_file():
return str(candidate)
return sys.executable
def main() -> int:
args = parse_args()
if args.extract_only and args.verify_only:
raise SystemExit("--extract-only and --verify-only are mutually exclusive")
script_root = Path(__file__).resolve().parent
trusted_root = script_root.parents[1]
input_pdf = Path(args.input_pdf).expanduser().resolve()
output_dir = Path(args.output_dir).expanduser().resolve()
extracted_dir = output_dir / "extracted"
verified_dir = output_dir / "verified"
document_path = extracted_dir / "document.blocks.json"
ocr_python = discover_ocr_python(args.ocr_python)
if not args.verify_only and (args.force_extract or not document_path.is_file()):
ocr_cmd = [ocr_python, str(script_root / "paper_ocr.py"), str(input_pdf), str(extracted_dir)]
if args.model_path:
ocr_cmd.extend(["--model-path", args.model_path])
if args.pages:
ocr_cmd.extend(["--pages", args.pages])
run_step(ocr_cmd, cwd=trusted_root)
elif not document_path.is_file():
raise SystemExit(f"missing extracted ledger at {document_path}; rerun without --verify-only")
if args.extract_only:
return 0
verify_cmd = [sys.executable, str(script_root / "paper_verify.py"), str(extracted_dir), str(verified_dir)]
if args.pages:
verify_cmd.extend(["--pages", args.pages])
run_step(verify_cmd, cwd=trusted_root)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|