Shorten and condense comments across the RAG backend, frontend, and tests for readability. Comment text only; no code, strings, identifiers, or logic changed. License headers and lint/type pragmas are preserved.
162 lines
5.5 KiB
Python
162 lines
5.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||
|
||
"""PDF → Markdown via pymupdf4llm; pypdf fallback for malformed files."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
from pathlib import Path
|
||
|
||
from . import ParsedImage, ParsedPage, ParseResult
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# pymupdf4llm wraps OCR'd vector-graphics text with these markers even when
|
||
# `ignore_images=True`. Strip the whole block — the VLM captioner describes the
|
||
# figure, and the marker text just pollutes the chunked body / citations.
|
||
_PICTURE_TEXT_BLOCK_RE = re.compile(
|
||
r"-{3,}\s*Start of picture text\s*-{3,}.*?-{3,}\s*End of picture text\s*-{3,}",
|
||
re.DOTALL | re.IGNORECASE,
|
||
)
|
||
|
||
|
||
def _strip_picture_text_markers(md: str) -> str:
|
||
return _PICTURE_TEXT_BLOCK_RE.sub("", md)
|
||
|
||
|
||
def _extract_with_pymupdf(path: Path, want_images: bool) -> ParseResult:
|
||
import pymupdf
|
||
import pymupdf4llm
|
||
|
||
doc = pymupdf.open(str(path))
|
||
try:
|
||
pages: list[ParsedPage] = []
|
||
for page_index in range(len(doc)):
|
||
try:
|
||
md = pymupdf4llm.to_markdown(
|
||
doc,
|
||
pages = [page_index],
|
||
write_images = False,
|
||
ignore_images = True,
|
||
show_progress = False,
|
||
)
|
||
except Exception:
|
||
# pymupdf4llm can choke on a single page; fall back to plain text.
|
||
md = doc[page_index].get_text("text") or ""
|
||
md = _strip_picture_text_markers(md).strip()
|
||
if md:
|
||
pages.append(ParsedPage(text = md, page_number = page_index + 1))
|
||
|
||
images: list[ParsedImage] = []
|
||
if want_images:
|
||
images = _extract_images_pymupdf(doc, pages)
|
||
return ParseResult(pages = pages, images = images)
|
||
finally:
|
||
doc.close()
|
||
|
||
|
||
# Pages smaller than this (in PDF points) are ignored as figure regions —
|
||
# bigger than a typical icon/glyph, smaller than a banner.
|
||
_MIN_FIGURE_PT = 60
|
||
# 2× scale renders at 144 dpi (PDF default is 72 dpi). Enough resolution for
|
||
# the captioner to read axis labels, arrow text, and inset photos.
|
||
_RENDER_SCALE = 2.0
|
||
# Expand the union bbox a few points so caption baselines / borders survive.
|
||
_FIGURE_MARGIN_PT = 8.0
|
||
|
||
|
||
def _extract_images_pymupdf(doc, pages: list[ParsedPage]) -> list[ParsedImage]:
|
||
"""Render each page's figure region (vector drawings + raster sub-images)
|
||
as a single PNG. Vector schematics like Figure 1 (no embedded raster)
|
||
are visible to the captioner only via rendering — ``page.get_images``
|
||
misses them entirely. We union all non-text geometry on a page into
|
||
one bbox; for academic papers this typically maps 1:1 to "the figure
|
||
on this page".
|
||
"""
|
||
import pymupdf
|
||
|
||
captions_by_page: dict[int, str] = {
|
||
p.page_number: p.text for p in pages if p.page_number
|
||
}
|
||
out: list[ParsedImage] = []
|
||
for page_index in range(len(doc)):
|
||
page = doc[page_index]
|
||
page_number = page_index + 1
|
||
try:
|
||
rects: list[pymupdf.Rect] = []
|
||
for drawing in page.get_drawings() or []:
|
||
rect = drawing.get("rect")
|
||
if rect is not None:
|
||
rects.append(pymupdf.Rect(rect))
|
||
for info in page.get_image_info(xrefs = True) or []:
|
||
bbox = info.get("bbox")
|
||
if bbox is not None:
|
||
rects.append(pymupdf.Rect(bbox))
|
||
except Exception:
|
||
continue
|
||
if not rects:
|
||
continue
|
||
union = rects[0]
|
||
for r in rects[1:]:
|
||
union |= r
|
||
if union.width < _MIN_FIGURE_PT or union.height < _MIN_FIGURE_PT:
|
||
continue
|
||
# Expand and clip to page rect so we don't render past page edges.
|
||
union = (
|
||
pymupdf.Rect(
|
||
union.x0 - _FIGURE_MARGIN_PT,
|
||
union.y0 - _FIGURE_MARGIN_PT,
|
||
union.x1 + _FIGURE_MARGIN_PT,
|
||
union.y1 + _FIGURE_MARGIN_PT,
|
||
)
|
||
& page.rect
|
||
)
|
||
try:
|
||
matrix = pymupdf.Matrix(_RENDER_SCALE, _RENDER_SCALE)
|
||
pix = page.get_pixmap(clip = union, matrix = matrix, alpha = False)
|
||
png_bytes = pix.tobytes("png")
|
||
except Exception:
|
||
continue
|
||
if not png_bytes:
|
||
continue
|
||
caption = (captions_by_page.get(page_number, "") or "")[:1500]
|
||
out.append(
|
||
ParsedImage(
|
||
image_bytes = png_bytes,
|
||
mime_type = "image/png",
|
||
page_number = page_number,
|
||
nearest_caption = caption,
|
||
)
|
||
)
|
||
return out
|
||
|
||
|
||
def _extract_with_pypdf_fallback(path: Path) -> ParseResult:
|
||
from pypdf import PdfReader
|
||
|
||
reader = PdfReader(str(path))
|
||
pages: list[ParsedPage] = []
|
||
for index, page in enumerate(reader.pages):
|
||
try:
|
||
text = page.extract_text() or ""
|
||
except Exception:
|
||
text = ""
|
||
text = text.strip()
|
||
if text:
|
||
pages.append(ParsedPage(text = text, page_number = index + 1))
|
||
return ParseResult(pages = pages, images = [])
|
||
|
||
|
||
def extract(path: Path, *, want_images: bool = False) -> ParseResult:
|
||
try:
|
||
return _extract_with_pymupdf(path, want_images)
|
||
except Exception as exc:
|
||
logger.warning(
|
||
"pymupdf failed for %s (%s: %s); falling back to pypdf",
|
||
path,
|
||
type(exc).__name__,
|
||
exc,
|
||
)
|
||
return _extract_with_pypdf_fallback(path)
|