|
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
12.4 kB
| # 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 mistral` produces the artifact in ~8 s. The | |
| > MinerU backends (`hybrid`/`vlm`/`pipeline`, everything below about GPU) are now the **fallback** β | |
| > both emit the identical `ParsedDocument`. Mistral needs `MISTRAL_API_KEY` (read from the repo | |
| > `.env`); its normalizer is `normalize_mistral.py`, chunking **section**-level by default. Recall on | |
| > the BUMA fixture: section **0.878** (`eval/parsing/README.md`). | |
| > **2026-09-24 β `paddleocr` backend, v2 document processing only.** `paddle_ocr.py` calls | |
| > Baidu AI Studio's PaddleOCR-VL job API (multipart submit β poll `state` until `done` β | |
| > JSONL `result.layoutParsingResults[]`, one per page); figures come back as hosted URLs and are | |
| > downloaded at parse time into `markdown.images_base64`, so the cache is self-contained. | |
| > `normalize_paddle.py` turns the layout blocks into the same `ParsedDocument`. Selected by the | |
| > app setting `DOCUMENT_PARSER_BACKEND`; the default here and the CLI's `--backend` choices are | |
| > unchanged, so offline knowledge runs never reach it. PDF and images only. Needs | |
| > `PADDLEOCR_ACCESS_TOKEN`. | |
| ## Files | |
| - `contracts.py` β **the seam.** `Chunk`, `ParsedDocument`, and the declared | |
| `Mention` / `TermRecord` handoff shapes. The one file both halves must agree on | |
| - `config.py` β all settings, and the single place the GPU backend flips | |
| - `parse.py` β parser wrapper (Mistral or MinerU) with a content-addressed parse cache | |
| - `mistral_ocr.py` β the Mistral OCR API call (upload β `ocr.process`); no GPU | |
| - `normalize.py` β MinerU's flat item list β meaningful chunks | |
| - `normalize_mistral.py` β Mistral's block stream β chunks (the Mistral counterpart of `normalize.py`) | |
| - `render.py` β LaTeX/HTML β readable prose for `Chunk.text` (shared by both normalizers) | |
| - `checks.py` β quality warnings, including silent-corruption detection | |
| - `manifest.py` / `report.py` β per-run record and throughput extrapolation | |
| - `run.py` β batch CLI: one folder in, artifacts + manifest out | |
| ## Use | |
| ```bash | |
| 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. | |
| > `subprocess` takes 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 warnings` is 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 in `manifest.json`. | |
| - **`ok / failed`** is the line to watch. Failed documents are recorded in full, | |
| with their traces, in `failures.jsonl`, and one failure does not stop the rest. | |
| Throughput numbers and their extrapolation: | |
| ```bash | |
| 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/pipeline` is 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. | |
| ```bash | |
| 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 run` uninstalls MinerU** β it syncs the venv to the lock first. | |
| Use `uv run --no-sync`, this repo's standing convention (see CLAUDE.md); | |
| `scripts/parse.py` already does. | |
| - If anyone runs `uv sync`, MinerU disappears. Reinstall with the command above; | |
| `scripts/parse.py` prints 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.0` is 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.py` warns 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`](../../docs/knowledge/KNOWLEDGE_PIPELINE_TODO.md) | |
| for the task breakdown and the seam discussion, and | |
| [`docs/knowledge/KNOWLEDGE_PIPELINE_CALIBRATION.md`](../../docs/knowledge/KNOWLEDGE_PIPELINE_CALIBRATION.md) | |
| Β§4 for the chunking constants this module follows. | |