Spaces:
Sleeping
Sleeping
| """Post-generation PDF parsing validation. | |
| Success is never declared on the visual PDF alone. After a résumé PDF is | |
| produced we re-extract its text with a real parser and verify the content is | |
| present, ordered, and parseable — the same way an ATS would read it — and that | |
| no hidden/injected keyword layer was smuggled in. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import Dict, List, Optional | |
| def _extract_pdf_text(pdf_path: str) -> Optional[str]: | |
| """Best-effort text extraction. Tries pdfplumber, then pymupdf. None on failure.""" | |
| try: | |
| import pdfplumber | |
| parts = [] | |
| with pdfplumber.open(pdf_path) as pdf: | |
| for page in pdf.pages: | |
| parts.append(page.extract_text() or "") | |
| text = "\n".join(parts).strip() | |
| if text: | |
| return text | |
| except Exception: | |
| pass | |
| try: | |
| import fitz # pymupdf | |
| doc = fitz.open(pdf_path) | |
| text = "\n".join(p.get_text() for p in doc).strip() | |
| doc.close() | |
| return text or None | |
| except Exception: | |
| return None | |
| def _pdftotext_extract(pdf_path: str) -> Optional[str]: | |
| """Independent parser #2: the poppler `pdftotext` CLI. None if unavailable.""" | |
| import shutil | |
| import subprocess | |
| exe = shutil.which("pdftotext") | |
| if not exe: | |
| return None | |
| try: | |
| out = subprocess.run([exe, "-layout", pdf_path, "-"], | |
| capture_output=True, timeout=30) | |
| txt = out.stdout.decode("utf-8", errors="replace") | |
| return txt if txt.strip() else None | |
| except Exception: | |
| return None | |
| def verify_keywords_two_parsers(pdf_path: str, keywords: List[str]) -> Dict: | |
| """Confirm each accepted keyword survives BOTH independent parsers (a Python | |
| lib + poppler's pdftotext). Flags any keyword split/corrupted/missing in | |
| either. Falls back to a second Python parser (pymupdf) when pdftotext is | |
| absent, so there are always two independent extractions.""" | |
| import re as _re | |
| py = _extract_pdf_text(pdf_path) or "" | |
| cli = _pdftotext_extract(pdf_path) | |
| parser2_name = "pdftotext" | |
| if cli is None: | |
| # fall back to a genuinely different Python engine | |
| try: | |
| import fitz | |
| doc = fitz.open(pdf_path) | |
| cli = "\n".join(p.get_text() for p in doc) | |
| doc.close() | |
| parser2_name = "pymupdf" | |
| except Exception: | |
| cli = "" | |
| def _present(text, kw): | |
| p = _re.sub(r"\s+", " ", (kw or "").lower()).strip() | |
| toks = [_re.escape(t) for t in p.split()] | |
| if not toks: | |
| return False | |
| pat = r"(?<![a-z0-9])" + r"[\s\W]{0,3}".join(toks) + r"(?![a-z0-9])" | |
| return _re.search(pat, text.lower()) is not None | |
| results = [] | |
| for kw in keywords: | |
| in_py, in_cli = _present(py, kw), _present(cli, kw) | |
| results.append({"keyword": kw, "parser1_pdfplumber": in_py, | |
| f"parser2_{parser2_name}": in_cli, | |
| "in_both": in_py and in_cli}) | |
| return { | |
| "parser1": "pdfplumber/pymupdf", "parser2": parser2_name, | |
| "parser1_chars": len(py), "parser2_chars": len(cli), | |
| "keywords_checked": len(keywords), | |
| "present_in_both": sum(1 for r in results if r["in_both"]), | |
| "missing_or_split": [r["keyword"] for r in results if not r["in_both"]], | |
| "per_keyword": results, | |
| } | |
| def validate_pdf( | |
| pdf_path: str, | |
| *, | |
| expected_sections: Optional[List[str]] = None, | |
| expected_contact: Optional[List[str]] = None, | |
| forbidden_markers: Optional[List[str]] = None, | |
| ) -> Dict: | |
| """Parse the generated PDF and verify readability/integrity. | |
| Returns a diagnostics dict (stored alongside the output). `ok` is True only | |
| when the parser recovered text, expected sections appear in order, contact | |
| details are present, and no injected/hidden marker leaked into the text. | |
| """ | |
| result: Dict = { | |
| "ok": False, "parser_recovered_text": False, "char_count": 0, | |
| "sections_found": [], "sections_missing": [], "sections_in_order": None, | |
| "contact_present": None, "forbidden_markers_found": [], "warnings": [], | |
| } | |
| text = _extract_pdf_text(pdf_path) | |
| if not text: | |
| result["warnings"].append("parser recovered no text (image-only or corrupt)") | |
| return result | |
| result["parser_recovered_text"] = True | |
| result["char_count"] = len(text) | |
| low = text.lower() | |
| # Section presence + order. Match a HEADING line (the section word dominating | |
| # its own line), not any prose substring — "communication skills" in a bullet | |
| # must not count as the SKILLS heading. | |
| expected_sections = expected_sections or ["experience", "education", "skills"] | |
| def _heading_pos(sec: str) -> int: | |
| s = sec.lower() | |
| for m in re.finditer(r"(?im)^[^\S\n]*([A-Za-z &/]{3,40})[^\S\n]*$", text): | |
| line = m.group(1).strip().lower() | |
| # near-exact heading line only (résumé headings stand alone) — never a | |
| # prose line that merely starts with the section word. | |
| if line == s or line.rstrip("s") == s.rstrip("s") \ | |
| or (line.startswith(s) and len(line) <= len(s) + 3): | |
| return m.start() | |
| return -1 | |
| positions = [] | |
| for sec in expected_sections: | |
| idx = _heading_pos(sec) | |
| if idx >= 0: | |
| result["sections_found"].append(sec) | |
| positions.append((sec, idx)) | |
| else: | |
| result["sections_missing"].append(sec) | |
| ordered_positions = [p for _, p in positions] | |
| result["sections_in_order"] = (ordered_positions == sorted(ordered_positions) | |
| if len(ordered_positions) > 1 else True) | |
| # Contact readability (any expected token present). | |
| if expected_contact: | |
| result["contact_present"] = any( | |
| re.sub(r"\s+", "", c.lower()) in re.sub(r"\s+", "", low) | |
| for c in expected_contact if c | |
| ) | |
| # No injected/hidden marker text (our own ATS comment tags or a hidden layer). | |
| markers = forbidden_markers or ["% ats-item", "% ats-skills-other", | |
| "ats-inject", "core focus areas include"] | |
| for m in markers: | |
| if m.lower() in low: | |
| result["forbidden_markers_found"].append(m) | |
| result["ok"] = ( | |
| result["parser_recovered_text"] | |
| and not result["sections_missing"] | |
| and result["sections_in_order"] is True | |
| and not result["forbidden_markers_found"] | |
| and (result["contact_present"] in (None, True)) | |
| ) | |
| if result["sections_missing"]: | |
| result["warnings"].append( | |
| f"sections not parseable: {result['sections_missing']}") | |
| if result["forbidden_markers_found"]: | |
| result["warnings"].append( | |
| f"injected/hidden markers leaked into text: " | |
| f"{result['forbidden_markers_found']}") | |
| return result | |
| if __name__ == "__main__": # ponytail: runnable self-check (needs a sample PDF) | |
| import os, sys | |
| sample = sys.argv[1] if len(sys.argv) > 1 else os.path.join( | |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | |
| "tests", "v2_output.pdf") | |
| if os.path.exists(sample): | |
| r = validate_pdf(sample, expected_sections=["experience", "education", "skills"]) | |
| print("pdf_validate self-check:", {k: r[k] for k in | |
| ("ok", "parser_recovered_text", "sections_found", | |
| "sections_missing", "forbidden_markers_found")}) | |
| else: | |
| print(f"pdf_validate self-check SKIPPED — no sample PDF at {sample}") | |