feat(literature): add trade-name resolver, Poucher/Arctander parsers, and PyMuPDF extraction scripts
0ebf67e unverified | from __future__ import annotations | |
| """ | |
| Client for the Unlimited-OCR Hugging Face Space. | |
| Uploads an image or PDF file and polls for extracted text. | |
| """ | |
| import json | |
| import logging | |
| import time | |
| from pathlib import Path | |
| from typing import Any | |
| import requests | |
| logger = logging.getLogger("pino.ocr_client") | |
| SPACE_API = "https://akhaliq-unlimited-ocr.hf.space/gradio_api" | |
| def upload_file(file_path: str | Path, token: str | None = None) -> dict[str, Any]: | |
| """Upload a file to the Space and return the Gradio FileData payload.""" | |
| path = Path(file_path) | |
| url = f"{SPACE_API}/upload" | |
| headers = {} | |
| if token: | |
| headers["Authorization"] = f"Bearer {token}" | |
| with path.open("rb") as f: | |
| response = requests.post(url, files={"files": f}, headers=headers, timeout=60) | |
| response.raise_for_status() | |
| data = response.json() | |
| # The upload endpoint returns a list of FileData objects. | |
| if isinstance(data, list): | |
| return data[0] | |
| return data | |
| def call_ocr(file_data: dict[str, Any], endpoint: str = "run_ocr", token: str | None = None) -> str: | |
| """Call the OCR endpoint and return the event_id to poll.""" | |
| url = f"{SPACE_API}/call/{endpoint}" | |
| headers = {"Content-Type": "application/json"} | |
| if token: | |
| headers["Authorization"] = f"Bearer {token}" | |
| # Ensure file_data is a proper Gradio FileData dict. | |
| if isinstance(file_data, str): | |
| file_payload = { | |
| "path": file_data, | |
| "url": None, | |
| "size": None, | |
| "orig_name": None, | |
| "mime_type": None, | |
| "is_stream": False, | |
| "meta": {"_type": "gradio.FileData"}, | |
| } | |
| else: | |
| file_payload = dict(file_data) | |
| if "meta" not in file_payload: | |
| file_payload["meta"] = {"_type": "gradio.FileData"} | |
| payload = {"data": [file_payload, "gundam", "document parsing"]} | |
| response = requests.post(url, json=payload, headers=headers, timeout=60) | |
| response.raise_for_status() | |
| data = response.json() | |
| logger.debug("OCR call response: %s", data) | |
| return str(data["event_id"]) | |
| def poll_result(event_id: str, endpoint: str = "run_ocr", token: str | None = None, timeout: float = 180.0) -> dict[str, Any]: | |
| """Poll the result endpoint until completion or timeout. | |
| Gradio streaming endpoints emit Server-Sent Events. We accumulate ``text`` | |
| chunks until ``done`` is true, then return the concatenated result. | |
| """ | |
| url = f"{SPACE_API}/call/{endpoint}/{event_id}" | |
| headers = {} | |
| if token: | |
| headers["Authorization"] = f"Bearer {token}" | |
| deadline = time.time() + timeout | |
| collected_text_parts: list[str] = [] | |
| while time.time() < deadline: | |
| response = requests.get(url, headers=headers, timeout=60) | |
| response.raise_for_status() | |
| done = False | |
| for line in response.text.splitlines(): | |
| line = line.strip() | |
| if not line or not line.startswith("data:"): | |
| continue | |
| try: | |
| payload = json.loads(line[5:].strip()) | |
| except json.JSONDecodeError: | |
| continue | |
| if isinstance(payload, dict): | |
| if payload.get("text"): | |
| collected_text_parts.append(str(payload["text"])) | |
| if payload.get("done"): | |
| done = True | |
| elif isinstance(payload, list) and len(payload) > 0: | |
| return {"result": payload} | |
| if done: | |
| return {"result": "".join(collected_text_parts)} | |
| time.sleep(1.0) | |
| raise TimeoutError(f"OCR polling timed out for event_id {event_id}") | |
| def extract_text(file_path: str | Path, token: str | None = None) -> str: | |
| """End-to-end: upload a file, run OCR, and return the extracted text.""" | |
| file_data = upload_file(file_path, token=token) | |
| event_id = call_ocr(file_data, endpoint="run_ocr", token=token) | |
| result = poll_result(event_id, endpoint="run_ocr", token=token) | |
| # The OCR result is typically a list of strings or a single string. | |
| if isinstance(result, dict) and "result" in result: | |
| data = result["result"] | |
| else: | |
| data = result | |
| if isinstance(data, list): | |
| return "\n".join(str(item) for item in data) | |
| return str(data) | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Extract text from an image/PDF using Unlimited-OCR") | |
| parser.add_argument("file", help="Path to image or PDF file") | |
| parser.add_argument("--token", default=None, help="Hugging Face token") | |
| parser.add_argument("--output", default=None, help="Optional output text file") | |
| args = parser.parse_args() | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| text = extract_text(args.file, token=args.token) | |
| if args.output: | |
| Path(args.output).write_text(text, encoding="utf-8") | |
| else: | |
| print(text) | |