Tôi xây pipeline OCR PDF scan 60 trang: chỉ dùng GPU khi cần
Với PDF dài 50–60 trang, tôi extract text layer trước và chỉ OCR các bản scan. Cách chia theo trang, gọi Gemma nội bộ qua Ollama và kiểm soát retry giúp pipeline dễ vận hành hơn.
Một PDF nội bộ dài 50–60 trang không phải lúc nào cũng cần đưa qua OCR.
Có file đã có sẵn text layer và có thể trích xuất bằng thư viện Python. Có file lại chỉ là ảnh scan được đóng gói trong PDF. Nếu OCR mọi tài liệu ngay từ đầu, GPU sẽ phải xử lý cả những phần việc mà code thông thường làm nhanh hơn.
Đó là nguyên tắc tôi dùng khi xây pipeline: extract được thì extract, chỉ OCR khi thực sự cần.
Luồng xử lý: phân loại trước, OCR sau
Pipeline cơ bản của tôi như sau:
text
PDF → kiểm tra text layer → Extract text hoặc OCR → ghép kết quả → lưu/xử lý tiếp
Khi nhận PDF, hệ thống kiểm tra trước xem có text layer hay không.
Nếu có thể lấy text, tôi extract trực tiếp bằng Python.
Nếu không lấy được text, tôi xem đó là PDF scan và chuyển sang OCR bằng model vision chạy nội bộ.
Điểm mấu chốt là không để AI làm thay việc mà thư viện thông thường đã xử lý được.
> Đừng OCR một tài liệu nếu máy có thể đọc trực tiếp text của nó.
Với số lượng lớn tài liệu, bước kiểm tra này giúp giảm lượng trang phải đưa vào GPU.
PDF scan: xử lý từng trang, không gửi cả file
Với PDF scan, tôi không gửi nguyên file 60 trang vào model. Thay vào đó, mỗi trang được render thành ảnh, encode Base64, gửi qua Ollama để lấy text rồi ghép kết quả theo thứ tự trang.
text
Upload PDF
│
▼
Check text layer
│
├── Có ──> Python extract text ──> Hoàn tất
│
└── Không
│
▼
PDF → Images
│
▼
for page in pages
│
├── page 1 → Ollama → text
├── page 2 → Ollama → text
├── page 3 → Ollama → text
│
...
└── page 60 → Ollama → text
│
▼
Merge text
Tách theo trang giúp pipeline dễ kiểm soát hơn:
Biết chính xác trang nào OCR thất bại.
Retry riêng một trang lỗi, không chạy lại toàn bộ PDF.
Giảm phạm vi xử lý của từng request.
Dễ theo dõi trạng thái và ghép lại kết quả theo page.
Khi mở rộng quy mô, đây cũng là nền tảng để tổ chức queue OCR, worker xử lý song song, retry theo trang và lưu trạng thái từng page.
OCR nội bộ bằng Ollama và Gemma
Phần OCR của tôi chạy trong hạ tầng nội bộ với Ollama + Gemma 4 26B.
Mỗi ảnh trang được encode Base64 và gửi tới Ollama qua API chat:
bash
curl --location 'http://<ollama-host>/api/chat' \
--header 'Content-Type: application/json' \
--data '{
"model": "gemma4:26b",
"messages": [
{
"role": "system",
"content": "Bạn là công cụ OCR. Chỉ xuất văn bản thuần UTF-8 theo đúng thứ tự đọc. Không JSON, không Markdown, không tọa độ hoặc bounding box, không mô tả ảnh. Không suy đoán chữ viết tay hoặc nội dung không nhìn rõ."
},
{
"role": "user",
"content": "Trích xuất toàn bộ chữ nhìn thấy trong ảnh. Chỉ trả về nội dung văn bản thuần.",
"images": ["<base64-image>"]
}
],
"stream": false,
"think": false
}'
Ở bước này, mục tiêu của model chỉ là OCR, không phải phân tích hay diễn giải tài liệu.
Vì vậy, prompt cần chặt:
Chỉ trả văn bản thuần UTF-8.
Giữ đúng thứ tự đọc.
Không JSON.
Không Markdown.
Không mô tả ảnh.
Không suy đoán phần chữ viết tay hoặc nội dung không rõ.
Nếu prompt quá mở, model vision có thể mô tả ảnh, thêm định dạng hoặc tự hoàn thiện phần chữ mờ. Những hành vi đó không phù hợp khi cần bản chép sát với tài liệu gốc.
Tôi cũng đặt:
json
"think": false
Bài toán ở đây là transcription, không cần reasoning. Điều cần ưu tiên là text được trả về sát với nội dung nhìn thấy trong trang scan.
Vì sao chạy model trong mạng nội bộ?
Kiến trúc tôi ưu tiên là:
text
Application → Ollama → Gemma → GPU nội bộ
Với tài liệu doanh nghiệp, đặc biệt là dữ liệu nội bộ, tôi không mặc định gửi từng trang scan lên OCR hoặc Vision API bên ngoài.
Chạy nội bộ giúp chủ động hơn về:
Model sử dụng.
Prompt OCR.
Giới hạn request.
Tài nguyên GPU.
Phạm vi lưu chuyển của tài liệu.
Đổi lại, đội triển khai phải tự vận hành các phần như queue, concurrency, timeout, retry và theo dõi GPU.
Những điểm cần chuẩn bị khi đưa vào production
Nếu tiếp tục mở rộng pipeline này, các phần tôi ưu tiên tối ưu không chỉ nằm ở model:
Tạo queue cho các trang chờ OCR.
Chạy worker song song ở mức phù hợp với VRAM.
Retry theo từng trang thay vì theo cả PDF.
Lưu trạng thái xử lý của từng page.
Giới hạn concurrency theo tài nguyên GPU thực tế.
Điểm quan trọng là mỗi trang nên là một đơn vị công việc độc lập: có đầu vào, trạng thái, kết quả hoặc lỗi riêng. Khi một request gặp sự cố, hệ thống chỉ cần xử lý lại đúng trang đó.
Checklist triển khai
[ ] Kiểm tra text layer trước khi gọi OCR.
[ ] Extract trực tiếp các PDF đã có text.
[ ] Render PDF scan thành ảnh theo từng trang.
[ ] Gửi từng page qua OCR thay vì gửi cả PDF dài.
[ ] Dùng prompt chặt để model chỉ trả text.
[ ] Ghép kết quả theo thứ tự trang.
[ ] Chuẩn bị retry, queue và lưu trạng thái theo page.
[ ] Giới hạn số tác vụ OCR đồng thời theo VRAM thực tế.
Bài học chính
Phần đáng giá nhất trong pipeline này không chỉ là chọn model OCR.
Đó là cách phân chia công việc: phần nào thư viện thông thường làm được thì để code xử lý; chỉ đưa những trang scan thực sự cần nhận diện chữ vào GPU.
text
┌─ Text PDF ──> Python Extract
PDF ──> Detect ──┤
└─ Scan PDF ──> Page Images
│
▼
Ollama + Gemma
│
▼
Text
> AI không nhất thiết phải xử lý toàn bộ pipeline. Hãy dùng AI đúng ở đoạn mà các phương pháp truyền thống xử lý không tốt.
Với OCR tài liệu, tối ưu hiệu quả đôi khi không phải là tìm model lớn hơn, mà là giảm số trang thực sự phải đưa vào model.