Repository navigation
fix(document): swap the AGPL rasterizer for pypdfium2 and bound OCR per page (#13896) - #14568
Conversation
Review — APPROVE-WITH-NITSThe licence question was checked independently, and further down than the PR itself went — which is the right depth for this, because a permissive wrapper around a copyleft blob would solve nothing:
So the copyleft genuinely does not leak, all the way down. Good call, and correctly motivated. The API translation is faithful, checked against pypdfium2's own source rather than inferred:
The per-page timeout is wired correctly. The swap is complete: repo-wide grep for Nits
What this does not close for #14510, confirmed unchangedStating it here so the stacked merge is not read as clearing my block:
Both of the first two remain reachable from an ordinary PDF upload. Your body and code comments scope themselves accurately and do not claim otherwise, which I appreciate. |
#14551) Code review found the requirements-ci-drift baseline claiming "no CI-scoped test exercising the real library" for 9 packages that had a real pytest.importorskip / importlib.util.find_spec gate, silently skipping -- the exact vacuous-pass shape this guard exists to end. Mirrors each into the matching requirements-ci/*.txt file instead of leaving it baselined: scikit-learn, ldap3, weasyprint, faiss-cpu, datasketch, tree-sitter-python, trafilatura, yt-dlp, ddgs. Auditing autobot_shared/ (the highest-risk shared surface, reached by both backends) the same way found two more of the same class that were not gated by a skip at all: bcrypt and pydantic-settings are unconditionally imported by autobot_shared/auth/jwt_core.py and autobot_shared/ssot_config.py respectively -- both declared in autobot_shared/pyproject.toml but never installed from there by CI (the same shape as the pre-existing PyJWT[crypto] comment in requirements-ci/security.txt), so importing either module, and therefore nearly anything that imports config or auth, was liable to fail collection outright. Mirrored rather than baselined. Resolves the pytesseract/#14510 collision: #14510 (open) already adds pytesseract to requirements-ci/document.txt with its own OCR-fallback rationale. Dropped the automation.txt addition from this PR and allowlisted pytesseract instead, pointing at #14510 -- the audit will force removal of that line the moment either PR lands it for real. Corrected the opencv-python-headless rationale: it is not actually an omission. requirements-ci/automation.txt already declares plain opencv-python, which installs the identical `cv2` import module computer_vision/template_matching_test.py's importorskip("cv2") needs. Widened tools/lint/check_ci_system_package_provisioning.py's detection past the literal shutil.which("x") shape: pytest.importorskip( is now also a recognised skip marker (the style scikit-learn/ldap3/etc. use), and a small PROBE_CALL_TO_BINARY table catches indirect probes like pytesseract.get_tesseract_version() that never call shutil.which at all -- so a real tesseract gate landing later is caught the day it appears instead of reading as "no real test, out of scope". Also fixed _test_files() to match pytest.ini's own `test_*.py` prefix convention, not only `*_test.py` suffixes -- the guard was under-scanning relative to what pytest actually collects. Reworded the docstring in check_ci_system_package_provisioning.py: it cited test_ci_actually_has_the_ocr_toolchain as an established #13896 pattern, but #13896 is still open (#14510/#14568) as of this writing -- corrected to describe the pattern without citing unlanded code.
ad56e9e to
1a66b06
Compare
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
CI: one gate fixed, one under investigation
So neither this change nor the branch it stacks on can plausibly reach that test, and the identical check passes on the base. Re-running the failed job to distinguish flaky from deterministic — if it reproduces, it is a real defect on this merge state and gets root-caused before this goes anywhere. Not merging until it is green either way. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
#14551) Code review found the requirements-ci-drift baseline claiming "no CI-scoped test exercising the real library" for 9 packages that had a real pytest.importorskip / importlib.util.find_spec gate, silently skipping -- the exact vacuous-pass shape this guard exists to end. Mirrors each into the matching requirements-ci/*.txt file instead of leaving it baselined: scikit-learn, ldap3, weasyprint, faiss-cpu, datasketch, tree-sitter-python, trafilatura, yt-dlp, ddgs. Auditing autobot_shared/ (the highest-risk shared surface, reached by both backends) the same way found two more of the same class that were not gated by a skip at all: bcrypt and pydantic-settings are unconditionally imported by autobot_shared/auth/jwt_core.py and autobot_shared/ssot_config.py respectively -- both declared in autobot_shared/pyproject.toml but never installed from there by CI (the same shape as the pre-existing PyJWT[crypto] comment in requirements-ci/security.txt), so importing either module, and therefore nearly anything that imports config or auth, was liable to fail collection outright. Mirrored rather than baselined. Resolves the pytesseract/#14510 collision: #14510 (open) already adds pytesseract to requirements-ci/document.txt with its own OCR-fallback rationale. Dropped the automation.txt addition from this PR and allowlisted pytesseract instead, pointing at #14510 -- the audit will force removal of that line the moment either PR lands it for real. Corrected the opencv-python-headless rationale: it is not actually an omission. requirements-ci/automation.txt already declares plain opencv-python, which installs the identical `cv2` import module computer_vision/template_matching_test.py's importorskip("cv2") needs. Widened tools/lint/check_ci_system_package_provisioning.py's detection past the literal shutil.which("x") shape: pytest.importorskip( is now also a recognised skip marker (the style scikit-learn/ldap3/etc. use), and a small PROBE_CALL_TO_BINARY table catches indirect probes like pytesseract.get_tesseract_version() that never call shutil.which at all -- so a real tesseract gate landing later is caught the day it appears instead of reading as "no real test, out of scope". Also fixed _test_files() to match pytest.ini's own `test_*.py` prefix convention, not only `*_test.py` suffixes -- the guard was under-scanning relative to what pytest actually collects. Reworded the docstring in check_ci_system_package_provisioning.py: it cited test_ci_actually_has_the_ocr_toolchain as an established #13896 pattern, but #13896 is still open (#14510/#14568) as of this writing -- corrected to describe the pattern without citing unlanded code.
#14551) Code review found the requirements-ci-drift baseline claiming "no CI-scoped test exercising the real library" for 9 packages that had a real pytest.importorskip / importlib.util.find_spec gate, silently skipping -- the exact vacuous-pass shape this guard exists to end. Mirrors each into the matching requirements-ci/*.txt file instead of leaving it baselined: scikit-learn, ldap3, weasyprint, faiss-cpu, datasketch, tree-sitter-python, trafilatura, yt-dlp, ddgs. Auditing autobot_shared/ (the highest-risk shared surface, reached by both backends) the same way found two more of the same class that were not gated by a skip at all: bcrypt and pydantic-settings are unconditionally imported by autobot_shared/auth/jwt_core.py and autobot_shared/ssot_config.py respectively -- both declared in autobot_shared/pyproject.toml but never installed from there by CI (the same shape as the pre-existing PyJWT[crypto] comment in requirements-ci/security.txt), so importing either module, and therefore nearly anything that imports config or auth, was liable to fail collection outright. Mirrored rather than baselined. Resolves the pytesseract/#14510 collision: #14510 (open) already adds pytesseract to requirements-ci/document.txt with its own OCR-fallback rationale. Dropped the automation.txt addition from this PR and allowlisted pytesseract instead, pointing at #14510 -- the audit will force removal of that line the moment either PR lands it for real. Corrected the opencv-python-headless rationale: it is not actually an omission. requirements-ci/automation.txt already declares plain opencv-python, which installs the identical `cv2` import module computer_vision/template_matching_test.py's importorskip("cv2") needs. Widened tools/lint/check_ci_system_package_provisioning.py's detection past the literal shutil.which("x") shape: pytest.importorskip( is now also a recognised skip marker (the style scikit-learn/ldap3/etc. use), and a small PROBE_CALL_TO_BINARY table catches indirect probes like pytesseract.get_tesseract_version() that never call shutil.which at all -- so a real tesseract gate landing later is caught the day it appears instead of reading as "no real test, out of scope". Also fixed _test_files() to match pytest.ini's own `test_*.py` prefix convention, not only `*_test.py` suffixes -- the guard was under-scanning relative to what pytest actually collects. Reworded the docstring in check_ci_system_package_provisioning.py: it cited test_ci_actually_has_the_ocr_toolchain as an established #13896 pattern, but #13896 is still open (#14510/#14568) as of this writing -- corrected to describe the pattern without citing unlanded code.
Thinking Path
Review of #14510 blocked on one thing: it adds PyMuPDF, which is AGPL-3.0-or-commercial, to a product that ships Apache-2.0. AGPL's copyleft reaches network use, which is exactly how a backend ingest service is consumed, and this repo has already answered the same question one notch weaker —
0d67a956eremoved a plain GPL-3.0 dependency "to align with Apache-2.0".Owner decision: swap the library. This is that swap, targeted at
issue-13896so #14510 can merge.It deliberately does not touch that branch. A locked worktree belongs to the session that wrote it, so this lands as its own branch to merge or cherry-pick rather than a push under an active session.
What Changed
pypdfium2(Apache-2.0/BSD, bundles pdfium, needs no system package) replaces PyMuPDF.media/document/ocr.pywas the only consumer, so the change is contained:fitz.open(stream=raw, filetype="pdf")pypdfium2.PdfDocument(raw)document.page_countlen(document)load_page(n).get_pixmap(dpi=dpi)document[n].render(scale=dpi / 72)Image.frombytes("RGB", (w, h), samples).to_pil()render(scale=)takes a multiple of 72 DPI, pdfium's unit, and yields a PIL image directly — so the explicitfrombytesround-trip is deleted rather than reimplemented, and thePIL.Imageimport goes with it.Also: the per-page timeout review flagged
pytesseract.image_to_stringaccepts atimeout=that was not being passed, so it was unbounded on a path that accepts arbitrary uploads.DEFAULT_MAX_OCR_PAGESbounds page count; nothing bounded per-page cost, and one page with a pathological MediaBox renders a very large bitmap however few pages were requested.New
AUTOBOT_DOCUMENT_OCR_PAGE_TIMEOUTknob, following the file's existing_positive_int_settingconvention exactly, default 60s. The rendered image is now closed explicitly too.Verification — read this part
The 6 skips are the end-to-end tests, and they cover exactly the code I rewrote. They need
pytesseract,pypdfium2and the tesseract binary; none is installed in this checkout, and installing them here is not something I will do. So:fitz/PyMuPDF.PdfDocument(raw),len(document),document[i],render(scale=...).to_pil(),close(). Those are correct against the pypdfium2 v4 API by reading, not by executing.CI installs the toolchain (
setup-python-suitealready installstesseract-ocr, andrequirements-ci/document.txtnow installspypdfium2), so the end-to-end tests will run there and are the real gate. Treat a green CI run as the verification this PR body cannot provide.That the tests skip rather than silently pass is the property
test_ci_actually_has_the_ocr_toolchainexists to protect — it fails loudly if the runner ever loses the toolchain, so a skip here cannot masquerade as a pass there.Model Used
Opus 5
Refs #13896, unblocks #14510