Download src/knowledge_parsing/README.md from DataEyond/Agentic-Service-Data-Eyond-Catalog: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/main/src/knowledge_parsing/README.md
- Command line
-
hf download hf://spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/src/knowledge_parsing/README.md
-
curl -L -o README.md https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/main/src/knowledge_parsing/README.md
knowledge_parsing
Document parsing β the first half of the knowledge pipeline. PDF/DOCX in, a
versioned ParsedDocument out.
The second half (src/knowledge_extraction/) consumes that artifact and never
the parser itself, which is what keeps MinerU swappable: a Tesseract or Azure
Document Intelligence path emits the same artifact and extraction never learns
which one ran.
Additive and flag-gated. The existing unstructured path (src/knowledge/,
Tesseract OCR β chunk β pgvector) is untouched and keeps running as-is.
Current (2026-09-02) β the default parser is now Mistral OCR.
--backend mistral(the config default) is an API with no GPU:run --backend mistralproduces the artifact in ~8 s. The MinerU backends (hybrid/vlm/pipeline, everything below about GPU) are now the fallback β both emit the identicalParsedDocument. Mistral needsMISTRAL_API_KEY(read from the repo.env); its normalizer isnormalize_mistral.py, chunking section-level by default. Recall on the BUMA fixture: section 0.878 (eval/parsing/README.md).
2026-09-24 β
paddleocrbackend, v2 document processing only.paddle_ocr.pycalls Baidu AI Studio's PaddleOCR-VL job API (multipart submit β pollstateuntildoneβ JSONLresult.layoutParsingResults[], one per page); figures come back as hosted URLs and are downloaded at parse time intomarkdown.images_base64, so the cache is self-contained.normalize_paddle.pyturns the layout blocks into the sameParsedDocument. Selected by the app settingDOCUMENT_PARSER_BACKEND; the default here and the CLI's--backendchoices are unchanged, so offline knowledge runs never reach it. PDF and images only. NeedsPADDLEOCR_ACCESS_TOKEN.
Files
contracts.pyβ the seam.Chunk,ParsedDocument, and the declaredMention/TermRecordhandoff shapes. The one file both halves must agree onconfig.pyβ all settings, and the single place the GPU backend flipsparse.pyβ parser wrapper (Mistral or MinerU) with a content-addressed parse cachemistral_ocr.pyβ the Mistral OCR API call (upload βocr.process); no GPUnormalize.pyβ MinerU's flat item list β meaningful chunksnormalize_mistral.pyβ Mistral's block stream β chunks (the Mistral counterpart ofnormalize.py)render.pyβ LaTeX/HTML β readable prose forChunk.text(shared by both normalizers)checks.pyβ quality warnings, including silent-corruption detectionmanifest.py/report.pyβ per-run record and throughput extrapolationrun.pyβ batch CLI: one folder in, artifacts + manifest out
Use
py scripts/parse.py # Windows
python3 scripts/parse.py # Linux
py scripts/parse.py data/other # a different folder
py scripts/parse.py --backend vlm # force a specific backend
The script sets the env vars that are required and picks a backend. Calling the
module directly works too, but then those env vars are yours to set β see
scripts/parse.py for the list.
The wrapper is Python, not shell, deliberately. Bash and PowerShell versions were written first and inside twenty minutes hit two traps that are specific to shells: CRLF line endings making bash fail on its very first line, and a trailing backslash on a path swallowing the argument AFTER it.
subprocesstakes a list of arguments and never goes through a shell, so neither is possible.
This is a development aid. Production calls the module directly β parsing is an admin-triggered offline batch, not something a person runs from a terminal.
A successful run looks like this:
>> No GPU detected β falling back to pipeline
(no image descriptions; multiplication copied as the letter 'x')
Run 20260826-131546 | backend=pipeline | 2 documents
[1/2] Maintenance Engineering Handbook (1)-1 β¦ ok (cache) | 1 chunk
[2/2] STD_2026_006_MNO β¦ ok 385.5s | 31 chunks | β 3 warnings
--- summary ---
ok / failed : 2 / 0
total pages : 10
β quality warnings: 3 β see manifest.json
How to read it:
ok (cache)means that document was not re-parsed. The cache key is document content + settings + MinerU version, so only what genuinely changed gets redone. Cached documents are excluded from the seconds/page average.β n warningsis NORMAL, not a failure. The document is still processed. Warnings flag quality worth a human look β formulas with no numbers, empty tables, charts with no context, and most importantly numbers that are absent from the source PDF. Details land inmanifest.json.ok / failedis the line to watch. Failed documents are recorded in full, with their traces, infailures.jsonl, and one failure does not stop the rest.
Throughput numbers and their extrapolation:
uv run --no-sync python -m src.knowledge_parsing.report --scale 6800
Backend
These are the MinerU backends β now the fallback (see the Mistral note at the top). They still apply whenever
--backend hybrid/vlm/pipelineis chosen.
Among the MinerU backends the default is hybrid + effort=high, decided 2026-08-24 after all
four were measured on the same GPU over the same 29 pages:
| Backend | s/page | Image descriptions | Multiplication sign |
|---|---|---|---|
hybrid/high β |
4.85 | yes | \times |
hybrid/medium |
1.88 | force-disabled by MinerU | \times |
pipeline |
2.67 | never | the letter x β fuses tokens |
vlm |
6.96 | yes | \times |
vlm and hybrid/high produce 100% identical output, but vlm is slower.
pipeline is the only one that copies the multiplication sign as a literal x,
which fuses MOHH x Qty x PA into a single token and takes five terms with it.
Determinism: settled 2026-08-26. Two genuine re-parses produced 0 differences across 50 chunks β image descriptions included, character for character. Frozen baselines do not drift. Parse time varies (model loading); the output does not.
Running on a laptop with no GPU
hybrid and vlm need a GPU. Without one the script falls back to pipeline on
its own and says so. That output is valid but lower quality β no image
descriptions, and the multiplication sign copied as a letter. Do not use it as a
reference measurement.
Calling the module directly without --backend stops with a message naming the
command to use instead, rather than an opaque torch error.
Installing MinerU
MinerU is an extra, not a core dependency β the agent service never parses a
document during a request, so the deployed Space does not carry it. Importing
this package does not import MinerU or torch; only parse.py does, when called.
UV_CACHE_DIR=D:/uv-cache uv pip install --python .venv mineru==3.4.4
β οΈ Not uv sync --extra knowledge-parsing. That fails: mineru needs
python-docx>=1.2.0 while this project pins 1.1.2. Locking it means raising
the shared pin, and that is a team decision β not taken yet.
So mineru is absent from uv.lock. Two consequences:
- A plain
uv rununinstalls MinerU β it syncs the venv to the lock first. Useuv run --no-sync, this repo's standing convention (see CLAUDE.md);scripts/parse.pyalready does. - If anyone runs
uv sync, MinerU disappears. Reinstall with the command above;scripts/parse.pyprints that command itself when it happens.
Where the large files live
All kept off the nearly-full C: drive:
| Model weights | D:/mineru-models/hf, D:/mineru-models/modelscope |
| Temp files | D:/mineru-models/tmp |
uv cache |
D:/uv-cache |
| Parse cache + run output | data/knowledge_cache/, data/knowledge_runs/ (gitignored) |
Naming documents β a convention, and why it is temporary
The file name is currently a primary key, not a label. doc_id is the file
name stem; chunk_id is built from doc_id; and term_id, formula_id,
rule_id and domain_id are all digests of doc_id (knowledge_extraction/ids.py).
Renaming a file after ingestion therefore changes every identifier derived from
it, and an expert's approve/reject decisions are keyed to those identifiers.
Until that changes, name documents like this:
<DOC-CODE>_Rev<N.N>.pdf
STD-2026-006-MNO_Rev0.0.pdf
MEH-2019_Ch03.pdf
- Stable prefix, varying suffix. The doc code stays fixed across revisions so
they can eventually be recognised as versions of one document;
_Rev1.0is the part that moves, and it is machine-readable rather than only human-readable. - No spaces. They force quoting in every command, and a trailing backslash on a quoted path has already swallowed an argument once in this repo.
- Never
(1)/(2). That suffix means "downloaded twice", not "version 2". Ingested as-is it becomes a second legitimate document with its own full set of identifiers. If the content is identical, delete one βrun.pywarns when two inputs share a hash. If the content genuinely differs, it is a revision: give it a real_Rev<N>. - Never rename after ingestion.
The long human title is not lost by any of this β it stays inside the document,
and normally appears as the first chunk's heading.
Why this is a stopgap, not a design
The convention holds only because we control the file names while parsing is an
admin-triggered batch. The moment this sits behind an upload endpoint β which is
planned, after eval and the playground β it stops holding: people will upload
Copy of SOP final FIX (2).pdf, and permanent knowledge-base identifiers should
never have depended on what someone typed into a Save dialog.
The fix is to stop deriving identity from the name. ParsedDocument already
carries content_hash, a SHA-256 of the file, so the identifier exists β it is
simply not the one being used:
doc_id content_hash key, stable, machine-facing
source_title the file name label, may change freely
That is a seam change (doc_id reaches chunk_id, Provenance and ids.py), so
it needs agreeing rather than doing. It is also cheapest now: the migration cost
rises with every document ingested and every expert decision recorded, because
re-keying orphans all of them. One consequence to handle when it lands β
Provenance currently shows doc_id to the reviewer, so a display title has to
travel with it or experts will be reading hashes.
parse._mineru_name exists for the same reason and is deliberately minimal: once
doc_id is a hash, every path is short by construction and that function is
deleted.
Three things that are easy to get wrong
Chunk.text must stay verbatim. Extraction's guardrail locates quoted spans
literally inside it. Reflowing or normalising the text makes the lookup fail and
the field goes silently null β which presents as a bad model, not as a parser
bug.
page_idx is 0-based, exactly as MinerU reports it, with no conversion
anywhere in the pipeline. Converting to 1-based is the UI's job, done once at
display time, so the artifact always matches the raw MinerU output kept beside
it in the cache.
Markup never goes in text. The term filter is an NER model reading prose;
MinerU writes formulas character-spaced (P u r c h a s i n g ~ c o s t s) and
tables as HTML, and neither yields a single mention. Measured on one document and
gold set, changing only the parse: raw markup scored recall 0.7561 against 0.8537
for plain text; rendering it back recovered 0.8293. The markup is preserved in
Chunk.latex and Chunk.table_html, because the formula branch needs that form.
Output layout
data/knowledge_cache/parse/<hash>-<config>-<version>/ untouched MinerU output
data/knowledge_runs/<run_id>/
βββ manifest.json backend, versions, timings, quality warnings
βββ failures.jsonl documents that failed, with traces
βββ <doc>/chunks.json the artifact
Both are gitignored β they are data, not code.
See docs/knowledge/KNOWLEDGE_PIPELINE_TODO.md
for the task breakdown and the seam discussion, and
docs/knowledge/KNOWLEDGE_PIPELINE_CALIBRATION.md
Β§4 for the chunking constants this module follows.