Skip to content
曾亮懿 ZengLiangYi Blog

Integrating PaddleOCR-VL: Building a Document Inspection Tool

Connect to Baidu PaddleOCR-VL using Python standard library: manage async polling, coordinate bounding boxes, and build a document proofreading UI.

曾亮懿

曾亮懿

8 min read
Table of contents Show Hide

A document parsing API has returned Markdown. Is the integration finished? For plain text extraction, that might be enough. But with two-column PDFs, tables spanning pages and images mixed into the text, there are more questions: Are paragraphs in the right order? Are any tables missing? Where did a particular passage come from in the source?

I built a local web tool that brings Baidu Cloud’s PaddleOCR-VL document parsing API and result inspection into one page. The left pane shows the source document or a reconstructed layout. The right pane shows reading order, Markdown and JSON. Clicking an element locates its page and region.

The tool is open source under the MIT license. Baidu Cloud performs the recognition; the local application handles API calls and displays the results, with no model download or GPU required. This article explains the implementation. It does not cover model training or present the interface as evidence of recognition accuracy.

Start with a working sequence of API calls

Most of the project lives in two files: server.py handles the local HTTP service and cloud requests, while index.html handles uploads and inspection. The backend uses http.server, urllib.request and json from the Python standard library, with no third-party Python packages. The frontend loads PDF.js and its Markdown renderer from a CDN, so the tool still depends on network access.

The process has six steps:

Browser reads the document and sends its filename and Base64 content
    ↓
Local service reads credentials and obtains an access token
    ↓
Submit a parsing job and receive task_id
    ↓
Poll until the job succeeds or fails
    ↓
Download the parsing JSON and Markdown
    ↓
Browser displays results and links elements to page regions

The browser calls only the local /api/parse endpoint. The Python service reads the Baidu API Key and Secret Key, so they do not need to be embedded in the frontend page.

The two request boundaries use different formats. The browser sends JSON to the local service; Python submits and queries Baidu jobs using POST + application/x-www-form-urlencoded. The _post_form() helper handles form encoding, network errors and JSON decoding. The request format at one boundary cannot simply be reused at the other.

This integration uses these job endpoint paths:

Submit: /rest/2.0/brain/online/v2/paddle-vl-parser/task
Query:  /rest/2.0/brain/online/v2/paddle-vl-parser/task/query

These paths belong to Baidu Cloud’s document parsing service. Use the matching API documentation to check credentials and request parameters, rather than mixing in examples for other PaddleOCR deployment options.

A submitted job is not yet a parsing result

A task_id from the submission endpoint means the job has been created. The service then queries its status until it reaches success or failed. Downloadable result links become available after success.

Here is a simplified version of the polling logic, with response structure checks omitted. query_document() is the project’s existing query helper:

deadline = time.monotonic() + MAX_WAIT_SECONDS

while time.monotonic() < deadline:
    payload = query_document(access_token, task_id)
    result = payload.get("result", {})
    status = result.get("status")

    if status == "success":
        return payload
    if status == "failed":
        raise BaiduAPIError(f"Parsing failed: {result.get('task_error')}")

    time.sleep(POLL_SECONDS)

raise BaiduAPIError("Timed out waiting for parsing")

The implementation sets a five-second polling interval and a 600-second waiting threshold. Using time.monotonic() measures elapsed time without letting system clock adjustments affect the deadline. This is not a strict ten-minute limit on the entire request: authentication, submission and downloads take additional time, and an in-flight query has its own timeout.

Another distinction matters: the cloud job is asynchronous, but the browser’s request to this demo remains open while the local service polls and downloads the results. The backend neither stores jobs in a database nor exposes a resumable job query endpoint to the browser. This works for individual evaluation; multiple users, reconnection and long-running jobs require further task management design.

After success, the code checks that both parse_result_url and markdown_url exist, then downloads the JSON and Markdown. The frontend receives content it can display instead of fetching those two main result URLs itself.

Downloading the main results does not complete an archive. Images referenced by Markdown or JSON may still use temporary cloud URLs. The tool does not persist results, so export anything you need to keep.

Reading order comes from the API; content may live elsewhere

The display starts with pages. Each page’s layouts contains layout elements, while the actual table and image content may live in separate tables and images collections.

If the interface only loops through layouts and displays layout.text, a detected table may appear empty in the right pane. Check the associated records before concluding that the API returned no content.

The tool joins records within a page using layout_id, with this content priority:

  1. For a matching table, use its Markdown or HTML.
  2. For a matching image, use its description or image URL.
  3. Otherwise, fall back to layout.text.

This preserves two kinds of information: layouts supplies order and position, while table or image records supply the element’s content.

The reading-order list follows the element order returned by the API instead of sorting by coordinates. In a two-column document, top-to-bottom and left-to-right ordering does not necessarily represent the intended reading sequence. Applying a custom sort during evaluation would also change the model output being inspected. The interface likewise retains the returned layout types, including unknown ones.

Map parsing coordinates back onto the page

Once the content is available, the next step is linking each result to its source region.

The tool normalizes different position formats into [x, y, width, height]. For polygon coordinates, it takes the minimum and maximum vertex coordinates to produce an axis-aligned bounding rectangle. That rectangle locates a region; it does not trace the exact outline of rotated text.

As the viewport changes, the displayed page changes size. Overlay boxes therefore use relative coordinates. Given parsing page dimensions pageWidth and pageHeight, the core calculation is:

const [x, y, width, height] = box;

const leftPercent = (x / pageWidth) * 100;
const topPercent = (y / pageHeight) * 100;
const widthPercent = (width / pageWidth) * 100;
const heightPercent = (height / pageHeight) * 100;

The source also constrains positions and minimum display sizes to keep very small regions visible. PDF canvas rendering handles device pixel ratio separately, while overlays follow the displayed page proportions.

This calculation assumes that the API’s page coordinates correspond to the source being displayed.

Fall back to layout reconstruction when coordinates do not match

coordinateSpaceMatches() checks the parsed page dimensions, rotation angle and aspect ratio against the source. The current implementation attempts a source overlay only when the angle is zero, the dimensions are valid, and the relative aspect-ratio difference is below 0.035.

This is a heuristic check, not precise image registration. A matching aspect ratio cannot establish that no cropping or offset occurred, so the overlay still needs visual inspection.

If the check fails, the tool reconstructs a layout using the parsed page dimensions, element positions and content. This path can also be used when PDF.js fails to load or the browser cannot preview a document format directly.

A reconstructed layout helps inspect spatial relationships between elements. It cannot substitute for the original document as proof of correct recognition. The interface distinguishes the source-plus-overlay view from the reconstructed PaddleOCR-VL layout so users know what they are seeing.

Connect both panes with page and element identifiers

Clicking an element in the right pane stores the target’s page index, layout_id and coordinates. If the target is on another page, the interface switches pages before updating the overlay and selected element card.

Card matching uses both the page index and layout_id, avoiding a match on another page based only on the element identifier. Line-level positioning reads span_boxes, allowing inspection of individual lines within a paragraph.

This makes inspection straightforward: notice a questionable result, click it, and compare it against the source on the left. There is no need to repeatedly search through a long Markdown document and a multi-page PDF.

Keep four views for different questions

Reading order, Markdown, Markdown source and JSON each answer a different question:

View What to inspect
Reading order Whether the element sequence makes sense and where each result belongs in the source
Markdown How headings, paragraphs and tables appear when read as a continuous document
Markdown source Whether a rendering issue comes from the Markdown itself, including its links and markup
JSON The fields, coordinates and relationships actually returned by the API

For example, if a table looks wrong in the Markdown view, inspect the source and then the table records in JSON. The issue may come from parsing or presentation. Retaining the original output makes that distinction possible.

The tool does not add business field extraction, regex corrections or section truncation. Evaluation starts with seeing what the cloud service returned, making it easier to decide what processing the application still needs.

Run the open-source example

Prepare Python 3.9 or later and Baidu Cloud OCR application credentials with access to the corresponding document parsing service. The following commands are for macOS or Linux:

git clone https://github.com/ZengLiangYi/paddleocr-vl-document-parser-demo.git
cd paddleocr-vl-document-parser-demo
cp .env.example .env

Fill in .env:

BAIDU_API_KEY=your_API_Key
BAIDU_SECRET_KEY=your_Secret_Key

Then run:

python3 server.py

Open http://127.0.0.1:8765, select a document and start parsing. Do not open index.html directly: the page needs the local service. Windows setup and startup commands are in the repository README.

The demo uploads Base64 content and checks file sizes against limits of 10 MiB for images and 50 MiB for other supported documents. It does not implement the file_url upload path. These describe the tool’s current implementation; consult Baidu’s documentation and your account settings for supported service formats, quotas and billing.

Documents are sent to Baidu Cloud and requests consume your account quota. The service listens on localhost by default and has no application accounts, access authentication or production-grade security design. It should not be exposed directly as a public upload service.

To reuse it in an application, start with the request sequence in server.py and the result association logic in index.html. Add task persistence, concurrency limits, upload validation and safe rendering of returned content as required by your application.

Source and references

This article describes the implementation at commit e633377. It does not include recognition accuracy or performance benchmarks.

Share:

From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.