Files
LLM-UI/scripts/ocr_kia_pdf.py

179 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
OCR KIA service manual PDF → structured JSON.
Usage:
python3 ocr_kia_pdf.py [--pages N] [--start N]
Output: kia_data/kia_ocr.json
"""
import pymupdf
import subprocess
import tempfile
import os
import json
import re
import sys
import argparse
from pathlib import Path
PDF_PATH = "/mnt/y/Torrents/KIA4_by_kiario.pdf"
OUT_DIR = Path(__file__).parent.parent / "kia_data"
OUT_JSON = OUT_DIR / "kia_ocr.json"
# OCR language: rus for Russian, eng for English terms
OCR_LANG = "rus+eng"
# Zoom factor for rendering (2 = 2x, better OCR quality)
ZOOM = 2
def ocr_page(pixmap, page_num=0) -> str:
"""Run tesseract on a pixmap, return text. Retry on timeout with explicit cleanup."""
# Save pixmap to temp file, close before OCR
fd, img_path = tempfile.mkstemp(suffix=".png")
os.close(fd)
pixmap.save(img_path)
for attempt in range(3):
proc = None
try:
proc = subprocess.Popen(
["tesseract", img_path, "stdout", "-l", OCR_LANG],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
stdout, _ = proc.communicate(timeout=60)
if proc.returncode != 0 and proc.returncode is not None:
raise RuntimeError(f"tesseract exit {proc.returncode}")
os.unlink(img_path)
return stdout.strip()
except subprocess.TimeoutExpired:
if proc:
proc.kill()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
pass
if attempt < 2:
print(f" p{page_num}: timeout, retry {attempt+2}/3...", flush=True)
else:
print(f" p{page_num}: SKIP (timeout after 3 retries)", flush=True)
os.unlink(img_path)
return ""
except Exception as e:
if proc:
proc.kill()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
pass
os.unlink(img_path)
print(f" p{page_num}: ERROR {e}", flush=True)
return ""
def is_section_header(line: str) -> bool:
"""
Heuristic: detect section/chapter headers.
Examples:
- "ГЛАВА 1. ОБЩИЕ СВЕДЕНИЯ"
- "1. ТЕХНИЧЕСКОЕ ОБСЛУЖИВАНИЕ"
- "ДВИГАТЕЛЬ"
"""
line = line.strip()
if not line or len(line) < 3:
return False
# Numbered chapter: "1.", "1.2", "ГЛАВА 1"
if re.match(r"^(ГЛАВА|РАЗДЕЛ|ЧАСТЬ)\s+\d+", line, re.IGNORECASE):
return True
if re.match(r"^\d+\.\s+[А-ЯA-Z]", line):
return True
if re.match(r"^\d+\.\d+\.?\s+[А-ЯA-Z]", line):
return True
# ALL CAPS, 3+ words, reasonable length (not a full sentence)
if line.isupper() and len(line.split()) >= 2 and 5 < len(line) < 100:
return True
return False
def parse_pdf(start_page=0, max_pages=None):
"""OCR all pages and structure into sections."""
doc = pymupdf.open(PDF_PATH)
total_pages = doc.page_count
end_page = min(total_pages, start_page + max_pages) if max_pages else total_pages
print(f"PDF: {total_pages} pages, processing {start_page}{end_page-1}", flush=True)
sections = []
current_section = {"title": "Начало", "pages": [], "text": ""}
mat = pymupdf.Matrix(ZOOM, ZOOM)
for i in range(start_page, end_page):
page = doc[i]
pix = page.get_pixmap(matrix=mat)
text = ocr_page(pix, page_num=i)
if not text:
print(f" p{i}: [empty]")
continue
# Try to detect section header on this page
lines = text.split("\n")
header_found = False
for line in lines[:5]: # check first 5 lines
if is_section_header(line):
# Save previous section
if current_section["text"].strip():
sections.append(current_section)
current_section = {"title": line.strip(), "pages": [], "text": ""}
header_found = True
break
current_section["pages"].append(i)
current_section["text"] += text + "\n"
if (i - start_page) % 20 == 0:
print(f" p{i}: {len(text)} chars, sections so far: {len(sections)}", flush=True)
# Save last section
if current_section["text"].strip():
sections.append(current_section)
doc.close()
return {
"source": PDF_PATH,
"total_pages": total_pages,
"pages_processed": end_page - start_page,
"sections": sections,
}
def main():
parser = argparse.ArgumentParser(description="OCR KIA PDF → JSON")
parser.add_argument("--pages", type=int, default=None, help="Max pages to process")
parser.add_argument("--start", type=int, default=0, help="Start page")
args = parser.parse_args()
OUT_DIR.mkdir(parents=True, exist_ok=True)
data = parse_pdf(start_page=args.start, max_pages=args.pages)
print(f"\nSections: {len(data['sections'])}")
total_chars = sum(len(s["text"]) for s in data["sections"])
print(f"Total chars: {total_chars:,}")
with open(OUT_JSON, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"Saved: {OUT_JSON} ({os.path.getsize(OUT_JSON)/1024/1024:.1f} MB)")
print("Done!")
if __name__ == "__main__":
main()