pdf-to-md — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited pdf-to-md (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.
Turn a PDF into Markdown. The right path depends on what the document is and whether an OCR API key is available:
paper-to-md bundle (Markdown +section_audit.json + article.json) so it can feed csag-extraction. Convert with the OCR API when a key is set, else LiteParse v2 locally.
with LiteParse v2 for a fast, local, no-key result. Stop there.
LiteParse must be v2 (run-llama/liteparse, the Rust rewrite with the LiteParse Python API and lit CLI). LiteParse v1 is a different, unsupported API. liteparse_to_md.py pins liteparse>=2,<3 and refuses to run on anything else, so uv run always provisions the right per-platform v2 binary inside the wheel — nothing to vendor or compile, and no API key. OCR is on by default (bundled Tesseract).
LiteParse output is a draft, not the deliverable. LiteParse is a mechanical parser: it has no native Markdown, infers headings from font size/weight, and introduces artifacts (split words, broken hyphenation, dropped author blocks, merged columns). Whenever LiteParse is the engine, the LLM running this skill is responsible for shaping that draft into the right form — see "Shape the LiteParse output" below. The OCR API engine needs far less shaping.
| Document | OCR key set? | Path |
|---|---|---|
| Scientific paper / manuscript | yes (OCR_API_KEY or NELLI_API_KEY) | Mode A, engine = OCR API |
| Scientific paper / manuscript | no | Mode A, engine = LiteParse v2 |
| Anything else (fast Markdown) | n/a | Mode B (LiteParse v2 only) |
Check for a key with printenv OCR_API_KEY NELLI_API_KEY. If the paper is scanned or layout-heavy and no key is set, LiteParse v2 still works (it OCRs locally), but layout fidelity is lower than the OCR API.
Produces, beside the input, for stem <stem>: <stem>.md, <stem>.section_audit.json, <stem>.article.json (and optionally <stem>.ocr.json, <stem>.job.json, figure_review/).
OCR API (preferred when a key is available; best layout fidelity):
uv run skills/pdf-to-md/scripts/ocr_api_job.py \
/path/to/input.pdf --output-dir /path/to/output-dirThe helper auto-detects a local OCR host (http://127.0.0.1:8002/ocr) and otherwise uses the remote API (https://api.newlineages.com/ocr). Override with OCR_BASE_URL or --base-url.
LiteParse v2 fallback (no key required):
uv run skills/pdf-to-md/scripts/liteparse_to_md.py \
/path/to/input.pdf --output-dir /path/to/output-dirIf you used the LiteParse engine, shape `<stem>.md` before continuing — see "Shape the LiteParse output" below. The downstream steps only work as well as the Markdown they read.
uv run skills/pdf-to-md/scripts/build_section_audit.py /path/to/output-dir/<stem>.md uv run skills/pdf-to-md/scripts/populate_article_json.py /path/to/output-dir/<stem>.mdThis is a first pass. Review and complete fields the heuristics miss (authors with superscripts, methods, references, figure interpretation) against the Markdown and the article schema.
figure_interpretation from captions plus the rendered pages:
uv run skills/pdf-to-md/scripts/render_pdf_pages_to_png.py \
/path/to/input.pdf --output-dir /path/to/output-dir/figure_review uv run skills/pdf-to-md/scripts/validate_article_json.py \
/path/to/output-dir/<stem>.article.json \
--scientific-paper \
--section-audit /path/to/output-dir/<stem>.section_audit.jsonResolve every reported error before stopping. A missing field that is genuinely absent from the source is fixed by confirming absence, not by inventing content.
You may also start Mode A from a Markdown file you already trust — skip step 1 and run steps 2–5 on that .md.
One step, fully local, no key:
uv run skills/pdf-to-md/scripts/liteparse_to_md.py \
/path/to/input.pdf --output-dir /path/to/output-dirUseful flags: --no-ocr (faster on text-based PDFs), --ocr-server-url URL (higher-accuracy OCR server), --target-pages "1-5,10", --max-pages N, --password PW. The converter detects the title and section headings from font size and weight, filters page furniture (watermarks, running headers, repeated footers), and reflows text into paragraphs — then shape the result (next section).
LiteParse v2 gives a fast first draft. Because it is mechanical, you (the LLM running this skill) must read <stem>.md against the rendered pages and bring it into the right shape before treating the conversion as done. Do not hand back raw script output. Fix what the heuristics cannot:
# is the real title, not a journal banner, DOI line, or"Downloaded from…" watermark; set it correctly if wrong or missing.
## Abstract,## Introduction, ## Methods, ## Results, ## Discussion, ## References, etc.) and demote false positives; keep reading order.
and fix hyphenation that did not rejoin across line breaks.
often drops or scrambles around superscripts and email addresses.
tables that collapsed into runs of text.
license boilerplate the filter missed.
For Mode A, after this Markdown cleanup run populate_article_json.py, then complete every article.json field the first-pass heuristics leave empty (authors, affiliations, methods, references, figure_interpretation) from the shaped Markdown and rendered pages, so validation passes for the right reasons — never by inventing content. For Mode B, the shaped Markdown is the deliverable.
| Task | Command |
|---|---|
| Is there an OCR key? | printenv OCR_API_KEY NELLI_API_KEY |
| Paper, key set | ocr_api_job.py INPUT.pdf --output-dir DIR |
| Paper, no key | liteparse_to_md.py INPUT.pdf --output-dir DIR |
| Any PDF, fast | liteparse_to_md.py INPUT.pdf --output-dir DIR --no-ocr |
| Section audit | build_section_audit.py DIR/<stem>.md |
| Article JSON | populate_article_json.py DIR/<stem>.md |
| Figure PNGs | render_pdf_pages_to_png.py INPUT.pdf --output-dir DIR/figure_review |
| Validate paper | validate_article_json.py DIR/<stem>.article.json --scientific-paper --section-audit DIR/<stem>.section_audit.json |
All commands run from the repo root with uv run skills/pdf-to-md/scripts/<name>. liteparse_to_md.py and render_pdf_pages_to_png.py carry PEP 723 inline dependencies (liteparse, pypdfium2) that uv run installs automatically; the remaining scripts are standard-library only.
LibreOffice; JPG/PNG/TIFF/etc. via ImageMagick).
.md with a clear # Title, anauthor/affiliation block, recognizable section headings (Abstract, Introduction, Methods, Results, Discussion, Conclusion, References), and figure/table captions starting with Fig./Figure/Table.
OCR_API_KEY or NELLI_API_KEY, plus curl.--output-dir (keep it outside this repository).<stem>.md, plus <stem>.ocr.json and <stem>.job.json provenance.<stem>.section_audit.json and <stem>.article.json;optionally figure_review/ PNGs. csag-extraction consumes <stem>.md and <stem>.article.json; everything else is provenance.
title, authors, affiliations,abstract, main, methods, figure_legends (list), figure_interpretation, references (list). See references/article_schema.md and references/article.yaml.
<stem>.job.jsonrecords tool_version 2.x for the LiteParse engine.
rejoined words, front matter, captions, references) — not handed back raw.
# title (or none, never awatermark), and is free of repeated page furniture.
validate_article_json.py --scientific-paper returns OK.title, authors, and main are populated for a real paper, or their absenceis confirmed against the source (do not fabricate).
figure_legends is populated andfigure_interpretation is filled (or an explicit no-interpretation note is recorded).
<stem>.job.json) records the engine, tool version, and OCR setting.Fast Markdown from a non-paper PDF:
uv run skills/pdf-to-md/scripts/liteparse_to_md.py report.pdf --output-dir /tmp/out --no-ocr
# -> /tmp/out/report.md (+ report.ocr.json, report.job.json)Full paper bundle with no OCR key (LiteParse v2 engine):
DIR=/tmp/paper
uv run skills/pdf-to-md/scripts/liteparse_to_md.py paper.pdf --output-dir "$DIR"
uv run skills/pdf-to-md/scripts/populate_article_json.py "$DIR/paper.md"
uv run skills/pdf-to-md/scripts/validate_article_json.py \
"$DIR/paper.article.json" --scientific-paper \
--section-audit "$DIR/paper.section_audit.json"uv run skills/pdf-to-md/scripts/liteparse_to_md.py (not uv run python ...), so uv reads the PEP 723 inline dependency. Offline: pip install 'liteparse>=2,<3' first.liteparse is installed. Reinstall with pip install 'liteparse>=2,<3'; the skill targets the run-llama Rust rewrite (LiteParse Python API), not the old v1 API.article_extraction re-derives the title from the body.--dpi, or point --ocr-server-url at EasyOCR/PaddleOCR; for best fidelity use the OCR API engine.OCR_API_KEY/NELLI_API_KEY, or use the LiteParse v2 engine instead.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.