digital-marketer / scripts /ocr_book.py
vivekchakraverty's picture
Initial deploy: full app with 4-tier keyword research, per-task models; RAG index served from a separate private dataset repo
f23046e verified
Raw
History Blame Contribute Delete
3.76 kB
"""
SUPERSEDED by scripts/build_index_from_crawler.py — kept for reference only.
See README.md for the current build path.
OCR a scanned (image-only) book PDF into the same crawl-style text layout the
rest of the corpus uses, then update its state.json so build_index picks it up.
Renders each page to a grayscale image with PyMuPDF and runs the Tesseract CLI
(image via stdin -> text via stdout). Used for books that extract_books.py
flagged status="low_yield_scanned".
Usage:
python ocr_book.py --pdf "../books/613142275-Understanding-Digital-Marketing.pdf"
python ocr_book.py --pdf ... --out ../crawled_books --dpi 300 --lang eng
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
import fitz # PyMuPDF
def slugify(name: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:80] or "book"
def name_hash(name: str) -> str:
return hashlib.sha1(name.encode("utf-8")).hexdigest()[:16]
def clean_text(text: str) -> str:
text = text.replace("\x0c", "\n")
text = re.sub(r"[ \t]+\n", "\n", text)
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r"(\w)-\n(\w)", r"\1\2", text)
return text.strip()
def ocr_page(png_bytes: bytes, tesseract: str, lang: str) -> str:
proc = subprocess.run(
[tesseract, "stdin", "stdout", "-l", lang, "--psm", "1"],
input=png_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
return proc.stdout.decode("utf-8", errors="ignore")
def main():
parser = argparse.ArgumentParser(description="OCR a scanned book PDF -> crawl-style text")
parser.add_argument("--pdf", required=True)
parser.add_argument("--out", default="../crawled_books")
parser.add_argument("--dpi", type=int, default=300)
parser.add_argument("--lang", default="eng")
parser.add_argument("--tesseract", default=shutil.which("tesseract") or "tesseract")
args = parser.parse_args()
pdf = Path(args.pdf)
if not pdf.exists():
sys.exit(f"PDF not found: {pdf}")
slug = slugify(pdf.stem)
h = name_hash(pdf.name)
out_dir = Path(args.out) / slug
pages_dir = out_dir / "pages"
pages_dir.mkdir(parents=True, exist_ok=True)
doc = fitz.open(str(pdf))
n = doc.page_count
zoom = args.dpi / 72.0
mat = fitz.Matrix(zoom, zoom)
print(f"OCR {pdf.name}: {n} pages @ {args.dpi} DPI (tesseract: {args.tesseract})", flush=True)
parts = []
t0 = time.time()
for i in range(n):
page = doc[i]
pix = page.get_pixmap(matrix=mat, colorspace=fitz.csGRAY)
png = pix.tobytes("png")
txt = ocr_page(png, args.tesseract, args.lang)
parts.append(txt)
if (i + 1) % 20 == 0 or i + 1 == n:
elapsed = time.time() - t0
rate = (i + 1) / elapsed
eta = (n - i - 1) / rate if rate else 0
print(f" {i+1:>4}/{n} pages ({rate:.2f} pg/s, ETA {eta/60:.1f} min)", flush=True)
doc.close()
text = clean_text("\n".join(parts))
(pages_dir / f"{h}.txt").write_text(text, encoding="utf-8")
status = "ok_ocr" if len(text) >= 800 else "low_yield_ocr"
state = {
h: {
"file": pdf.name,
"category": "general",
"status": status,
"pages": n,
"chars": len(text),
"engine": f"tesseract-ocr@{args.dpi}dpi",
"extracted_at": time.time(),
}
}
(out_dir / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
print(f"\nDone: {len(text):,} chars, status={status} -> {pages_dir / (h + '.txt')}", flush=True)
if __name__ == "__main__":
main()