Spaces:
Sleeping
Sleeping
| import re | |
| from pathlib import Path | |
| import fitz # PyMuPDF | |
| from app.core.logging import get_logger | |
| logger = get_logger(__name__) | |
| # If extracted text is fewer than this many characters it's almost certainly | |
| # a scanned / image-only PDF that we cannot handle in MVP. | |
| SCANNED_THRESHOLD = 100 | |
| class ExtractionError(Exception): | |
| """Raised when the PDF cannot be meaningfully extracted.""" | |
| def extract_text(path: Path) -> tuple[str, int]: | |
| """ | |
| Extract and clean text from a digital PDF. | |
| Returns | |
| ------- | |
| text : str | |
| Cleaned, whitespace-normalised body text. | |
| page_count : int | |
| Number of pages in the document. | |
| Raises | |
| ------ | |
| ExtractionError | |
| If the file is unreadable or appears to be a scanned/image PDF. | |
| """ | |
| try: | |
| doc = fitz.open(str(path)) | |
| except Exception as exc: | |
| logger.error("Could not open PDF %s: %s", path, exc) | |
| raise ExtractionError("Could not read this PDF.") from exc | |
| page_count = len(doc) | |
| raw_pages: list[str] = [] | |
| for page in doc: | |
| raw_pages.append(page.get_text("text")) # type: ignore[arg-type] | |
| doc.close() | |
| full_text = "\n".join(raw_pages) | |
| cleaned = _clean(full_text) | |
| if len(cleaned) < SCANNED_THRESHOLD: | |
| logger.warning( | |
| "PDF %s yielded only %d chars — likely scanned", path, len(cleaned) | |
| ) | |
| raise ExtractionError( | |
| "This looks like a scanned resume; MVP supports digital text PDFs only." | |
| ) | |
| logger.info("Extracted %d chars from %d-page PDF %s", len(cleaned), page_count, path.name) | |
| return cleaned, page_count | |
| def _clean(raw: str) -> str: | |
| """Normalise whitespace and remove common PDF artifacts.""" | |
| # Collapse runs of whitespace that aren't newlines | |
| text = re.sub(r"[ \t]+", " ", raw) | |
| # Collapse 3+ consecutive blank lines into 2 | |
| text = re.sub(r"\n{3,}", "\n\n", text) | |
| # Strip leading/trailing whitespace per line | |
| lines = [line.strip() for line in text.splitlines()] | |
| return "\n".join(lines).strip() | |