Skip to main content

doccheck

Check a document before you accept it. doccheck takes a PDF or Word file and checks whether it is safe, readable, complete and on-topic (life sciences by default). It answers with accepted, review or rejected, plus the reasons.

from doccheck import ValidationEngine

result = ValidationEngine().validate("specification.pdf")
print(result.decision, result.relevance_score)     # accepted 94

Features

  • File checks: type, renamed files, size, password protection, damage.
  • Safety: PDF JavaScript and launch actions; Word macros, ActiveX, embedded programs, remote templates.
  • Page checks: empty, low-content and scanned pages; too many empty pages means rejected.
  • Relevance: BM25 keyword scoring, with a bundled list of 2,994 life-sciences keywords, or your own.
  • Any OCR for scanned pages:
    • cloud: Azure, AWS, Google;
    • local: Tesseract, PaddleOCR, EasyOCR, docTR, RapidOCR;
    • any web service or program;
    • your own plugin.
  • Explainable: every score traces back to its keywords and pages. Nothing is stored unless you ask.

Installation

pip install doccheck                   # Python 3.10+; checks PDF and Word files
pip install "doccheck[tesseract]"      # + an OCR engine, only if you need to read scanned pages

Available extras:

  • OCR: azure, aws, google, cloud (all three), tesseract, paddleocr, easyocr, doctr, rapidocr.
  • Keywords: xlsx (Excel keyword files).
  • Notes: Tesseract also needs the Tesseract program. The http and command engines need no extra.

Usage

from doccheck import ValidationConfig, ValidationEngine

engine = ValidationEngine(ValidationConfig(           # create once, reuse for every file
    ocr_provider="tesseract",
    max_file_size_mb=50,
    extra_keywords={"nitrosamine risk assessment": 0.9},
))

result = engine.validate("dossier.pdf")               # or validate_bytes(name, data); async versions too
print(result)
dossier.pdf: REVIEW (relevance 97%, 7 pages)
  To review (accept only after a person confirms):
    - EMPTY_PAGES: 1 of 7 pages (14%) is empty, with no text or images (page 3).
    - LOW_TEXT: 1 of 7 pages (14%) has less content (page 6).

The other cases print like this:

report.pdf: REJECTED (relevance 0%, 10 pages)
  Rejected because:
    - DOCUMENT_NOT_RELEVANT: The document does not match the expected life sciences content (match 0%). ...
qos.pdf: ACCEPTED (relevance 99%, 58 pages)
  No issues found.

Using the result in code:

  • result.decision and result.is_valid give the outcome.
  • result.errors and result.warnings give the reasons.
  • result.relevance_score gives the score.

Input: a PDF or .docx file, as a path or as bytes. Other types are rejected with a reason.

Output: a ValidationResult. result.to_dict() gives it as JSON, and never includes the document text.

{
  "filename": "batch-specification.pdf",
  "decision": "review",
  "relevance_score": 97,
  "errors": [],
  "warnings": [{"code": "EMPTY_PAGES", "message": "1 of 7 pages (14%) is empty, with no text or images (page 3).", "pages": [3]}],
  "matched_keywords": [{"keyword": "3 2 p 3", "weight": 1.0, "occurrences": 6, "score": 6.5153}],
  "page_results": [{"page": 1, "content": "text", "read_by": "text", "evidence": 0.8745, "relevant": true}],
  "keyword_fingerprint": "bdc18b90b40baee3",
  "extractor_version": "1"
}

What the decision means:

  • rejected: at least one error, e.g. UNSAFE_CONTENT, DOCUMENT_NOT_RELEVANT, TOO_MANY_EMPTY_PAGES.
  • review: warnings only, e.g. EMPTY_PAGES, CONTAINS_IMAGES, LOW_RELEVANCE.
  • accepted: nothing found.

Traceability:

  • keyword_fingerprint identifies the exact keyword list used.
  • extractor_version identifies the version of the page-reading rules.

Configuration

Every setting has a default, so you only set what you change. Settings can be given in code, as above, or in a .env file, which keeps keys out of the code:

DOCCHECK_OCR_PROVIDER=azure
DOCCHECK_OCR_ENDPOINT=https://<name>.cognitiveservices.azure.com/
DOCCHECK_OCR_KEY=<key>
DOCCHECK_OCR_MAX_PAGES=5
engine = ValidationEngine.from_env()        # environment variables win over .env
Main settings Default Meaning
ocr_provider none OCR engine for scanned pages
ocr_endpoint, ocr_key, ocr_options – Engine address, key and options, e.g. {"lang": "eng"}
ocr_max_pages 10 Most scanned pages sent to OCR per document (caps the cost)
keyword_source default default, none, a file (CSV/TSV/TXT/JSON/XLSX), or a dict
extra_keywords, remove_keywords – Add to or take from the keyword list
max_file_size_mb 100 Largest accepted file
relevance_accept_threshold / relevance_review_threshold 70 / 40 Score bands: relevant / review / rejected
result_store none none, jsonl:<path>, or your object with save(result)

See all 34 settings, their defaults, your values and what each one does:

  • doccheck settings in the terminal;
  • print(config.explain()) in code.

Other commands:

  • doccheck env-template > .env writes a template with every setting.
  • doccheck check-ocr tests the OCR connection with one page.

Mistakes fail when the engine is created, before any file is processed:

ConfigError: Unknown setting 'ocr_max_page' (did you mean 'ocr_max_pages'?).
ConfigError: ocr_provider='azure' needs ocr_endpoint and ocr_key ...
ConfigError: The OCR engine needs 'paddleocr', which is not installed: pip install doccheck[paddleocr]

OCR engines:

  • Cloud: azure, aws, google.
  • Local: tesseract, paddleocr, easyocr, doctr, rapidocr.
  • Universal: http (any OCR web service), command (any OCR program, e.g. {"command": "tesseract {image} - -l eng"}).
  • Your own: register_ocr_provider("name", YourClass).

Keywords:

  • Format: two columns, keyword,weight, with the weight between 0 and 1.
  • Weights: 0.5 or more marks a strong, specialist term; lower weights help the score but can't make a page relevant alone.
  • Check a file first: check_keyword_file(path) lists refused rows with reasons.

How it works

 file ─► 1. File checks ─► 2. Safety ─► 3. Read pages ─► 4. Text enough? ─► 5. Score ─► decision
                                         (PyMuPDF /        no ─► OCR a few
                                          python-docx)          scanned pages
  1. File checks. Type, real content (not just the extension), size, password and damage. A failure here rejects the file at once.

  2. Safety. Scripts, macros, embedded programs and remote content are looked for before any content is read. Any found rejects the file.

  3. Read pages, locally and for free.

    • PDF: PyMuPDF reads the text of every page, where the pictures are, and what sits in the header and footer bands.
    • Word: python-docx and lxml do the same.

    Each page is classed as text, low content, empty, header-only or scanned (pictures, no usable text). Too many pages without content rejects the file.

  4. OCR only when needed.

    • When it's skipped: text pages are used as they are. If they are enough (at least 3, making up at least half the document), no OCR is used at all.
    • When it's used: a random sample of the scanned pages is sent to the OCR engine. That's 25% of them, at least 3 and at most 10, as one batch.
    • What counts: a page counts only if OCR reads at least 150 characters from it.
    • One retry: if under half of those pages could be read, one more sample of other pages is tried, once.
    • Word files: their pictures are put into a small PDF for the same OCR.

    This keeps the OCR cost and time low, even for documents with hundreds of scanned pages.

  5. Score. The text that was read is matched against the keyword list. Each page gets a BM25 score.

    • Document score: 50% from the typical page, 30% from the share of relevant pages, 20% from keyword variety. So one keyword-dense page can't carry a whole document.
    • Decision: ≥ 70 is relevant, 40–70 is review, < 40 is rejected.

Design choices:

  • Cheap work first. File and safety checks and local text reading run before any OCR.
  • OCR is capped. It only runs when needed, on a few pages, which bounds both cost and time.
  • Fails closed. An unexpected error rejects the file instead of letting it through.
  • Nothing is written to disk, and every step is recorded in the result (page_results, extraction_log), so each decision can be traced.

Scope

For: document intake, where it stops wrong, unsafe or unusable files at upload:

  • regulatory and eCTD systems;
  • QMS;
  • supplier portals;
  • clinical and pharmacovigilance intake.

It also suits bulk screening of archives. It is life sciences by default, and works for any subject with your own keyword list.

Not for:

  • checking that content is correct;
  • telling document types apart (a specification from a stability report);
  • virus scanning;
  • extracting text.

Accuracy and limitations

Repeatability:

  • Text documents: the same file and settings always give the same result. There's no AI model or randomness.
  • Scanned documents: OCR reads a random sample of pages, so scores can vary by a few points between runs.

Measured with the bundled list on documents not used for tuning (text only, by relevance score):

Documents Relevant (≥70) Review (40–70) Not relevant (<40)
Regulatory dossier documents (128) 81% 13% 5%
Other life-sciences documents (10) 6 2 2
Unrelated documents (14) 0 0 100%

This is a guide, not a guarantee: test on your own documents.

Limitations:

  • Keywords match English (a–z) text only.
  • Very short documents can score low.
  • Logos and signatures trigger CONTAINS_IMAGES, which leads to review.
  • Word page numbers are estimated when the file wasn't saved by Word.
  • Without OCR, scanned pages aren't read.

Data and privacy

Documents and page text stay in memory and are never saved. Results are saved only if you set result_store.

Only a cloud or http OCR sends anything off the machine: the sampled scanned pages, at most ocr_max_pages.

Development

uv pip install -e ".[dev]"
python -m pytest && python -m ruff check src tests

Changes are listed in CHANGELOG.md, which ships with the source package.

License

doccheck is released under the MIT License (see the LICENSE file).

Note on PyMuPDF: doccheck depends on PyMuPDF to read PDFs, and PyMuPDF has its own licence: AGPL-3.0, or a commercial licence from Artifex. If you distribute a product built with doccheck, or offer it as a network service, PyMuPDF's terms apply to that product.

Metadata

Release files for doccheck 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for doccheck 0.1.0
File Size Uploaded Provenance
doccheck-0.1.0.tar.gz 123.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for doccheck 0.1.0
File Interpreter ABI Platform
doccheck-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 228.4 kB

Release files / doccheck-0.1.0.tar.gz

Download URL doccheck-0.1.0.tar.gz
Size 123.1 kB
Tags Source
SHA-256 checksum
How to use checksums
fa4a6aa572810fd23221a475eb3ccf2e954468d58e722509f7b99c2e73e45834
BLAKE2b-256 checksum
How to use checksums
1c41f5395bf7b7390ad1ba9c781402ade6303329a9b40b52eb82ccf83a009495
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Release files / doccheck-0.1.0-py3-none-any.whl

Download URL doccheck-0.1.0-py3-none-any.whl
Size 105.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
74e9babc4d083f807b2589e6225f4293635f0c6b98b50cf70eed5e4b6f0f6f01
BLAKE2b-256 checksum
How to use checksums
47b0eef317147d2d4f409cdb9ae91107d99821826998583a0584dcc86e6b9437
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Security for doccheck 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page