Parse PDFs with OpenDataLoader (layout-aware markdown + external images).
| 289 | |
| 290 | |
| 291 | class OpenDataLoaderParser(BaseParser): |
| 292 | """Parse PDFs with OpenDataLoader (layout-aware markdown + external images).""" |
| 293 | |
| 294 | def __init__(self, *args: Any, **kwargs: Any): |
| 295 | self._engine_overrides: Dict[str, Any] = { |
| 296 | k: v |
| 297 | for k, v in kwargs.items() |
| 298 | if k.startswith("odl_") or k in ("mineru_endpoint", "mineru_api_key") |
| 299 | } |
| 300 | super().__init__(*args, **kwargs) |
| 301 | |
| 302 | def parse_into_text(self, content: bytes) -> Document: |
| 303 | ok, msg = opendataloader_available(self._engine_overrides) |
| 304 | if not ok: |
| 305 | raise RuntimeError(msg) |
| 306 | |
| 307 | safe_name = os.path.basename(self.file_name) or "document.pdf" |
| 308 | if not safe_name.lower().endswith(".pdf"): |
| 309 | safe_name = f"{os.path.splitext(safe_name)[0] or 'document'}.pdf" |
| 310 | pdf_stem = os.path.splitext(safe_name)[0] |
| 311 | |
| 312 | max_workers = CONFIG.odl_max_workers |
| 313 | with parser_worker_limit("opendataloader", max_workers): |
| 314 | with tempfile.TemporaryDirectory(prefix="weknora-odl-") as tmp_dir: |
| 315 | pdf_path = os.path.join(tmp_dir, safe_name) |
| 316 | with open(pdf_path, "wb") as f: |
| 317 | f.write(content) |
| 318 | image_dir = os.path.join(tmp_dir, "images") |
| 319 | os.makedirs(image_dir, exist_ok=True) |
| 320 | |
| 321 | _run_convert( |
| 322 | pdf_path, |
| 323 | tmp_dir, |
| 324 | image_dir, |
| 325 | overrides=self._engine_overrides, |
| 326 | ) |
| 327 | |
| 328 | md_path = _find_markdown_file(tmp_dir, pdf_stem) |
| 329 | with open(md_path, encoding="utf-8", errors="replace") as f: |
| 330 | text = f.read() |
| 331 | |
| 332 | images = _collect_images_under_output(tmp_dir) |
| 333 | text = _rewrite_markdown_image_refs(text, images) |
| 334 | |
| 335 | if len(text.strip()) < _MIN_CHARS_PER_PAGE: |
| 336 | logger.info( |
| 337 | "OpenDataLoaderParser: %s yielded little text; " |
| 338 | "falling back to builtin scanned renderer", |
| 339 | self.file_name, |
| 340 | ) |
| 341 | from docreader.parser.pdf_parser import PDFScannedParser |
| 342 | |
| 343 | return PDFScannedParser( |
| 344 | file_name=self.file_name, file_type=self.file_type |
| 345 | ).parse_into_text(content) |
| 346 | |
| 347 | logger.info( |
| 348 | "OpenDataLoaderParser: %s -> content_len=%d images=%d", |
no outgoing calls