---
title: "Integrating PaddleOCR-VL: Building a Document Inspection Tool"
description: "Connect to Baidu PaddleOCR-VL using Python standard library: manage async polling, coordinate bounding boxes, and build a document proofreading UI."
image: "https://zengliangyi.com/og/blog/en/paddleocr-vl-api-proofreading.png"
---

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](https://github.com/ZengLiangYi/paddleocr-vl-document-parser-demo). 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:



```text
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:



```text
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](https://cloud.baidu.com/doc/OCR/s/Qmncwhwdt) 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:



```python
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:

- For a matching table, use its Markdown or HTML.

- For a matching image, use its description or image URL.

- 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:



```javascript
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:



```bash
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`:



```dotenv
BAIDU_API_KEY  =  your_API_Key  
  BAIDU_SECRET_KEY  =  your_Secret_Key
```



Then run:



```bash
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](https://github.com/ZengLiangYi/paddleocr-vl-document-parser-demo#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B).

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`](https://github.com/ZengLiangYi/paddleocr-vl-document-parser-demo/tree/e63337742b9548c70a6aa8b1a43533bcfbacab7c). It does not include recognition accuracy or performance benchmarks.

- [Backend requests and polling](https://github.com/ZengLiangYi/paddleocr-vl-document-parser-demo/blob/e63337742b9548c70a6aa8b1a43533bcfbacab7c/server.py)

- [Frontend result association, coordinates and previews](https://github.com/ZengLiangYi/paddleocr-vl-document-parser-demo/blob/e63337742b9548c70a6aa8b1a43533bcfbacab7c/index.html)

- [Baidu Cloud document parsing API](https://cloud.baidu.com/doc/OCR/s/Qmncwhwdt)

- [Access token documentation](https://ai.baidu.com/ai-doc/REFERENCE/Ck3dwjhhu)

- [Project overview on this site](/en/projects/paddleocr-vl/)

Share:

##  From writing to code

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

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Integrating PaddleOCR-VL: Building a Document Inspection Tool",
  "description": "Connect to Baidu PaddleOCR-VL using Python standard library: manage async polling, coordinate bounding boxes, and build a document proofreading UI.",
  "url": "https://zengliangyi.com/en/blog/paddleocr-vl-api-proofreading/",
  "image": "https://zengliangyi.com/images/blog/paddleocr-vl-api-proofreading.svg",
  "datePublished": "2026-09-19T00:00:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/paddleocr-vl-api-proofreading/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Integrating PaddleOCR-VL: Building a Document Inspection Tool",
      "item": "https://zengliangyi.com/en/blog/paddleocr-vl-api-proofreading/"
    }
  ]
}
```
