pdf-parse-qa — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited pdf-parse-qa (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A structured way to A/B test PDF parsers — VLMs, OCRs, layout APIs, anything that takes a PDF page and returns text + bounding boxes. You bring the parsers (HuggingFace models, vendor APIs, local libs), the skill brings the validator UI, the data contract, and adapter examples.
Picking a PDF parser is a benchmark problem disguised as a procurement problem. Vendor demos pick the easy pages. Per-block accuracy varies wildly by label (a parser may nail Title and Section-header but butcher Table or Picture). A summary "F1 score" hides that.
This skill gives you the per-block, per-label, source-anchored audit that vendor benchmarks don't.
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ your PDF │ → │ your parser │ → │ blocks.json │ → │ validator UI │
│ (CORS host) │ │ (one of adapters)│ │ (this contract) │ │ (vote ↑↓123) │
└─────────────┘ └──────────────────┘ └─────────────────┘ └──────────────┘
↓
verdicts.json
→ spreadsheet / Langfuseembed.okrapdf.com (GitHub raw, R2, S3 with CORS, gist).adapters/*.py) to produce blocks.json matching the data contract below. Host it the same way. https://embed.okrapdf.com/e/af8cc13f3d270c4b32f9245a?pdfUrl=<YOUR_PDF>&dataUrl=<YOUR_BLOCKS>&docTitle=Apple%2010-K%20(reducto)↑↓ to navigate, 1/2/3 to score pass/partial/fail, 0 clears. Bias toward pass; mark only deviations. Bbox cross-highlights on the canvas as you go.(parser, label) → pass-rate. That's the bake-off currency.Adapters MUST emit JSON in this shape. Coordinates are 0–1 normalized (origin = top-left), so they survive any PDF render scale.
{
"pages": [
{
"page_number": 1,
"blocks": [
{
"label": "Title",
"value": "Form 10-K",
"bbox": { "x": 0.10, "y": 0.05, "w": 0.80, "h": 0.03 }
},
{
"label": "Table",
"value": "Title of each class | Name of each exchange ...",
"bbox": { "x": 0.08, "y": 0.32, "w": 0.84, "h": 0.18 }
}
]
},
{ "page_number": 2, "blocks": [ ... ] }
]
}Required: pages[].page_number, pages[].blocks[].bbox.{x,y,w,h}. Recommended: pages[].blocks[].label, pages[].blocks[].value.
A starter file lives at examples/sample-blocks.json — sanity-test the widget before writing an adapter.
https://embed.okrapdf.com/e/af8cc13f3d270c4b32f9245a
?pdfUrl=<CORS-open PDF URL>
&dataUrl=<CORS-open blocks.json URL>
&docTitle=<display title, URL-encoded>
&scale=1.5 (optional, default 1.5 — pdf.js render scale)Verdicts persist in sessionStorage keyed by dataUrl, so refreshing the tab won't lose your work.
Reference scripts in adapters/. Each one: takes a PDF file path, writes blocks.json in the contract above. ~30–80 lines. Add your own — PRs welcome.
| File | Parser | Auth | Notes |
|---|---|---|---|
adapters/gemini-flash.py | Google Gemini 3 Flash (Thinking) | GEMINI_API_KEY | Uses the ParseBench prompt; native [y_min, x_min, y_max, x_max] bbox order normalized to {x,y,w,h} |
adapters/qwen2-vl-hf.py | HuggingFace Qwen/Qwen2.5-VL-7B-Instruct | none (local) | Local GPU; ~8GB VRAM @ fp16. Latest open-weights VL model. |
adapters/reducto.py | Reducto /parse | REDUCTO_API_KEY | One of the strongest commercial table extractors as of 2026. |
adapters/llamaparse.py | LlamaIndex LlamaParse (Premium / Agentic modes) | LLAMA_CLOUD_API_KEY | Stalwart; agentic mode handles charts. |
adapters/azure-di.py | Azure Document Intelligence (prebuilt-layout) | AZURE_DI_ENDPOINT, AZURE_DI_KEY | Enterprise default; strong layout, weaker on free-form OCR. |
adapters/okra.py | OkraPDF facets (any of 14: gemini-3-flash-minimal, reducto-parse, llamaparse-premium, mistral-ocr, mineru, unstructured, chandra-ocr, ...) | OKRA_API_KEY | One adapter, 14 parsers — useful if you already have an OkraPDF account. |
mistral-ocr-latest is fast and cheap; good Mistral-vs-Gemini cost angleanalyze_document with FORMS|TABLES|LAYOUTunstructured.partition.pdf with hi_resSame PDF, N parsers. Each adapter writes blocks-<parser>.json. Host all of them. Open one validator tab per parser. Score each. Aggregate by (parser, label) → pass-rate. That's a buyer-grade comparison your CFO can read.
# Same source PDF, four parsers in parallel
PDF=https://raw.githubusercontent.com/you/repo/main/test.pdf
python adapters/gemini-flash.py test.pdf > blocks-gemini.json
python adapters/reducto.py test.pdf > blocks-reducto.json
python adapters/llamaparse.py test.pdf > blocks-llamaparse.json
python adapters/qwen2-vl-hf.py test.pdf > blocks-qwen25vl.json
# Upload to gist / R2 / S3 (CORS open) — then:
for P in gemini reducto llamaparse qwen25vl; do
echo "https://embed.okrapdf.com/e/af8cc13f3d270c4b32f9245a?pdfUrl=$PDF&dataUrl=https://.../blocks-$P.json&docTitle=$P"
doneOpen each URL in a tab, score every page, copy verdicts, paste into:
| parser | Title pass | Table pass | Picture pass | Section-header pass | overall |
|---|---|---|---|---|---|
| gemini | 19/20 | 8/12 | 4/4 | 22/22 | 53/58 (91%) |
| reducto | 18/20 | 11/12 | 3/4 | 22/22 | 54/58 (93%) |
| ... |
Copy verdicts as JSON → clipboard:
{
"source": { "dataUrl": "https://.../blocks-reducto.json", "pdfUrl": "https://.../test.pdf" },
"docTitle": "Apple 10-K (reducto)",
"verdicts": [
{ "page": 1, "blockIndex": 0, "verdict": "pass" },
{ "page": 1, "blockIndex": 7, "verdict": "fail" },
{ "page": 2, "blockIndex": 3, "verdict": "partial" }
],
"capturedAt": "2026-05-18T05:40:52Z"
}When run against an OkraPDF facet (no dataUrl), source becomes { "docId": "...", "facet": "..." } instead. Same shape, different provenance.
| Key | Action |
|---|---|
↑ / k | previous block |
↓ / j | next block |
1 | mark pass |
2 | mark partial |
3 | mark fail |
0 | clear verdict |
| Click row | focus block |
| Click bbox | focus row |
Show source | re-scroll + flash the focused block |
The widget runs in the browser at embed.okrapdf.com and fetches your URLs cross-origin. They must respond with Access-Control-Allow-Origin: * (or include embed.okrapdf.com).
| Host | Works out-of-the-box | Notes |
|---|---|---|
GitHub raw (raw.githubusercontent.com) | ✅ | Use for small JSON + sample PDFs in your repo |
GitHub gist (gist.githubusercontent.com/.../raw/...) | ✅ | Best for one-off blocks.json — gh gist create blocks.json --public |
| Cloudflare R2 with public bucket + CORS | ✅ | Best for many large PDFs |
AWS S3 with CORS policy AllowedOrigin: * | ✅ | Add <CORSRule> to bucket |
| Vercel/Netlify static | ✅ | Default headers are CORS-open |
| Local file:// | ❌ | Browser blocks cross-origin file:// — host even short-lived runs via python -m http.server + a tunnel |
For private PDFs you don't want to host: see Standalone mode below.
The widget is a single HTML file. Clone the repo's parent (this skill ships SKILL.md only; the widget source lives at okrapdf/manual-app under embeds/parse-validator/) and open dist/index.html directly. Same ?pdfUrl=&dataUrl= params work. Use this for confidential PDFs that can't leave your machine.
okra-curl skill in this repo) — if you want a hosted parser store rather than running 6 SDKs yourself, OkraPDF's /v1/documents/{id}/facets/{name}/run exposes 14 parsers behind one API. Use the adapters/okra.py adapter then.MIT. Adapter scripts are reference implementations; PRs welcome at okrapdf/skills.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.