PaddleOCR-VL Text Recognition
This example adapts Baidu's PaddleOCR-VL model from GPU to the RKNN NPU platform, managed by systemd ocr.service, exposing a single-image recognition endpoint via curl /ocr.
Overall block diagram:
Client curl → :7880/ocr
│
▼
ocr_server.py (systemd ocr.service)
├─→ vision encoder → RK1828 NPU (507ms)
├─→ vision-mlp_AR → RK1828 NPU
├─→ LLM decoder → RK1828 NPU (168ms)
└─→ response JSON {text, vision_ms, llm_ms, total_ms, tokens}Two processing domains:
- RK3588: HTTP service process, file IO, JSON encode/decode
- RK1828 (PCIe co-processor): three-stage inference — vision encoder + mlp_AR + LLM decoder — connected to the Host via
/dev/dri/renderD128(PCIe address0004:41:00.0)
1. Install dependencies
# 1. NPU node
ls -l /dev/dri/renderD128
# 2. rknn3 systemd service
systemctl is-active rknn3
# 3. Python dependencies (rknn3lite / cv2 / gguf)
python3 -c "import rknn3lite, cv2; from gguf import GGUFReader; print('OK')"
# 4. Model + service unit
ls /userdata/models/Qwen3-VL-2B/PaddleOCR-VL/
ls -l /etc/systemd/system/ocr.serviceMeasured output (excerpt):
crw-rw---- 1 root video 226, 128 8月27日 11:11 /dev/dri/renderD128
active
OK
PaddleOCR-llm PaddleOCR-vision
-rw-r--r-- 1 root root 477 8月27日 11:11 /etc/systemd/system/ocr.serviceThe NPU node / rknn3.service / model / service unit are all in place.
The
ggufPython package is installed under/home/linaro/.local/lib/python3.11/site-packages/(user-local install for linaro); root's default PYTHONPATH cannot find it — see section 2.
2. Required service-file modification
The SDK's ocr.service runs as root, but the gguf package is in the linaro home directory, invisible to root → startup fails with ModuleNotFoundError: No module named 'gguf'.
Fix: add User=linaro + PYTHONPATH in the [Service] section:
[Service]
Type=simple
User=linaro
Group=linaro
Environment=PYTHONUSERBASE=/home/linaro/.local
Environment=PYTHONPATH=/home/linaro/.local/lib/python3.11/site-packages
ExecStart=/usr/bin/python3 /userdata/models/Qwen3-VL-2B/ocr_server.py
Environment=OCR_HOST=127.0.0.1
Environment=OCR_PORT=7880
Restart=on-failure
RestartSec=10Automatic modification command (run once manually on first deployment):
sudo cp /etc/systemd/system/ocr.service /etc/systemd/system/ocr.service.bak.$(date +%Y%m%d)
sudo sed -i '/^\[Service\]$/a User=linaro\nGroup=linaro\nEnvironment=PYTHONUSERBASE=/home/linaro/.local\nEnvironment=PYTHONPATH=/home/linaro/.local/lib/python3.11/site-packages' /etc/systemd/system/ocr.service
sudo systemctl daemon-reload
sudo systemctl restart ocr3. Model files
/userdata/models/Qwen3-VL-2B/PaddleOCR-VL/
├── llm/
│ ├── PaddleOCR-llm.rknn 16M LLM structure
│ ├── PaddleOCR-llm.weight 243M LLM weights
│ ├── PaddleOCR-llm.embed.bin 202M LLM word embeddings
│ └── PaddleOCR-llm.tokenizer.gguf 2.3M tokenizer
└── vision/
├── PaddleOCR-vision.rknn 28M vision encoder structure
├── PaddleOCR-vision.weight 261M vision encoder weights
├── PaddleOCR-vision-mlp_AR.rknn 213K mlp_AR structure
└── PaddleOCR-vision-mlp_AR.weight 15M mlp_AR weights~765 MB total; runtime RSS ~1.2 GB.
4. One-click deployment
cd /home/linaro/paddleocr-vl
bash install.sh # interactive (precheck → y/n → verify)
bash install.sh --yes # skip confirmationinstall.sh prechecks 3 things: the 8 model files / the 3 Python packages / the ocr.service unit. No files are installed — pure reuse + verification.
5. Daily usage
5.1 make status — systemd status
make statusMeasured output (excerpt):
● ocr.service - PaddleOCR-VL OCR HTTP Service
Loaded: loaded (/etc/systemd/system/ocr.service; disabled; preset: disabled)
Active: active (running) since ...
Main PID: 12345 (python3)
Tasks: 8 (limit: 7653)
Memory: 1.2G
CPU: 5.132s5.2 make health — health check
make healthMeasured output:
{
"status": "ok",
"model": "PaddleOCR-VL"
}5.3 make ocr — run one recognition
make ocr # uses the SDK-bundled test.jpgInput image (SDK-bundled test.jpg):

Measured output:

PaddleOCR-VL verified end to end: vision encoder + LLM decoder inference is correct and Chinese text is recognized.
5.4 Format of the text field
<fcel> / <lcel> / <nl> are PaddleOCR-VL's special markers (similar to HTML table tags) indicating recognition-box positions. The actual text content sits between the tags (in this example 群号:1025468710).
To clean it up, use a client-side regex:
import re
text = re.sub(r'<[^>]+>', '', raw_text)Output:
群号:10254687105.5 make restart / make logs
make restart # systemctl restart ocr (model load takes ~60s)
make logs # journalctl -u ocr -n 20
make stop # systemctl stop ocr (temporary stop)6. HTTP API
6.1 GET /health
curl -s http://127.0.0.1:7880/healthResponse:
{ "status": "ok", "model": "PaddleOCR-VL" }6.2 POST /ocr
Request:
{ "image": "<base64 of image>" }Response:
{
"text": "<fcel>群号:1025468710<lcel><lcel><nl>",
"vision_ms": 507,
"llm_ms": 168,
"total_ms": 676,
"tokens": 18
}An overly long base64 exceeds the command-line limit; the Makefile uses a temp file:
python3 -c "import json,base64; print(json.dumps({'image':base64.b64encode(open('test.jpg','rb').read()).decode()}))" > /tmp/ocr_req.json
curl -s -X POST http://127.0.0.1:7880/ocr -H "Content-Type: application/json" --data @/tmp/ocr_req.json7. Performance
| Stage | Measured time |
|---|---|
| vision encoder | 507 ms |
| vision-mlp_AR | ~1 ms |
| LLM decoder (18 tok) | 168 ms |
| Total inference | 676 ms |
| End to end (incl. HTTP) | ~700 ms |
8. Key facts quick reference
| Item | Value |
|---|---|
| Model path | /userdata/models/Qwen3-VL-2B/PaddleOCR-VL/ |
| HTTP port | 7880 |
| systemd unit | /etc/systemd/system/ocr.service |
| Runtime user | linaro (required, otherwise gguf not found) |
| API | GET /health / POST /ocr |
| Input | { "image": "<base64>" } |
| Output | { text, vision_ms, llm_ms, total_ms, tokens } |
| Text contains special tags | <fcel> / <lcel> / <nl> (client must clean) |
| Runtime RSS | ~1.2 GB |
| Total model file size | ~765 MB |
| SDK bug | set_chat_template ValueError: -2 (does not affect /ocr) |
9. FAQ
| Symptom | Cause | Fix |
|---|---|---|
ocr.service fails to start with ModuleNotFoundError: No module named 'gguf' | Running as root cannot find gguf under /home/linaro/.local/ | Add User=linaro + Environment=PYTHONPATH=/home/linaro/.local/lib/python3.11/site-packages per section 2 |
/ocr returns text containing <fcel> and similar tags | SDK output is raw, with position markers | Clean client-side with re.sub(r'<[^>]+>', '', text) |
set_chat_template ValueError: -2 | Known SDK bug | Service still listening; /ocr unaffected |
| Port occupied | 7880 already taken by another process | Find the occupier with lsof -i :7880, or change OCR_PORT in the service |
make ocr reports Argument list too long | base64 too long for the command line | The Makefile already uses the --data @/tmp/ocr_req.json temp file; just run make ocr |
| OOM, out of memory | RSS 1.2 GB, board has only 3.8 GB | Add 4 GB swap (same as the qwen3-vl steps) |
Not ready 60s after restart | Model loading takes time | Verify with sleep 60 && make health |
10. Known limitations
set_chat_template ValueError: -2: SDK bug; the service still listens and internal prompt handling may fall back to defaults; in testing the/ocrendpoint is unaffected<fcel>and similar position tags: require client-side cleanup- Same vision-mlp_AR SDK bug: shares vision-mlp_AR with qwen3-vl; in testing, vision alone runs fine (when vision-mlp_AR errors)
Related docs
- RTSP Streaming + AI Analysis — camera + NPU + RTSP end to end
