# 📋 Quy Trình Trích Xuất OCR — Cloud Vision Async PDF → RAW JSON

**Cập nhật:** 2026-05-16
**Áp dụng:** Tam-Bảo, Vi-Diệu-Pháp, Chuẩn-Mực-Sadi, và các văn bản Phật pháp Myanmar
**Mục tiêu:** Gửi nguyên file PDF lên Google Cloud Storage → Cloud Vision xử lý async → lưu các file JSON raw (3 trang/file — giảm từ 5 để tránh agent quá tải)
**Công cụ:** Python + Google Cloud Vision API (`async_batch_annotate_files`) + Google Cloud Storage

---

## 🔧 Thiết Lập

### Service Account

- **Key file:** `/opt/openclaw/.openclaw/workspace/google_service_account.json`
- **Project ID:** `zen-490314`
- **Service Account:** `openclaw-service@zen-490314.iam.gserviceaccount.com`

### Quyền cần có

Service Account cần các role sau:

| Role | Mục đích |
|------|----------|
| `roles/visionai.user` | Gọi Cloud Vision API |
| `roles/storage.objectAdmin` | Upload PDF lên GCS, đọc kết quả output |

### Cài đặt Python Packages

```bash
pip install google-cloud-vision google-cloud-storage
```

### Biến môi trường

```bash
export GOOGLE_APPLICATION_CREDENTIALS="/opt/openclaw/.openclaw/workspace/google_service_account.json"
```

### Tạo GCS Bucket

```bash
# Bucket chứa PDF input + JSON output (dùng chung 1 bucket)
gsutil mb -l asia-southeast1 gs://zen-ocr-pdf/
```

> Dùng region `asia-southeast1` (Singapore) để gần Việt Nam, giảm latency.

---

## 📦 Tổng Quan Quy Trình

```
PDF gốc (local)
  │
  ├─(1)─► Upload lên GCS (gs://zen-ocr-pdf/VDP-181-319.pdf)
  │
  ├─(2)─► Gọi Cloud Vision async_batch_annotate_files
  │         - Input:  GCS URI của PDF
  │         - Output: GCS URI đích cho JSON kết quả
  │         - Feature: DOCUMENT_TEXT_DETECTION
  │         - batch_size: 3 (mỗi file JSON chứa 3 trang)
  │         - timeout: 420s (~7 phút)
  │
  ├─(3)─► Đợi operation hoàn thành
  │
  └─(4)─► Download tất cả JSON files về local → ocr/raw/
            (KHÔNG gộp — giữ nguyên từng file)
```

**batch_size=3** — Chia nhỏ mỗi file JSON chứa 3 trang để DeepSeek/LLM dễ đọc và xử lý từng batch nhỏ.

---

## Bước 1: Upload PDF lên GCS

```python
from google.cloud import storage

def upload_pdf(local_pdf_path, bucket_name, destination_blob_name=None):
    """Upload file PDF lên Google Cloud Storage"""
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)
    
    if destination_blob_name is None:
        destination_blob_name = os.path.basename(local_pdf_path)
    
    blob = bucket.blob(destination_blob_name)
    blob.upload_from_filename(local_pdf_path)
    
    gcs_uri = f"gs://{bucket_name}/{destination_blob_name}"
    print(f"✅ Uploaded: {local_pdf_path} → {gcs_uri}")
    return gcs_uri
```

---

## Bước 2: Gọi Cloud Vision Async (Script Chính)

### Script: `scripts/ocr_pdf_to_raw.py`

```python
import os
import time
from google.cloud import vision
from google.cloud import storage

def ocr_pdf_to_raw_json(gcs_source_uri, gcs_destination_uri):
    """
    Gửi nguyên file PDF từ Google Storage cho Cloud Vision xử lý Async.
    
    Args:
        gcs_source_uri:      e.g. "gs://zen-ocr-pdf/VDP-181-319.pdf"
        gcs_destination_uri: e.g. "gs://zen-ocr-pdf/output/VDP-181-319/"
    
    Returns:
        operation result
    """
    client = vision.ImageAnnotatorClient()

    feature = vision.Feature(
        type_=vision.Feature.Type.DOCUMENT_TEXT_DETECTION
    )

    # Cấu hình file đầu vào (PDF)
    input_config = vision.InputConfig(
        gcs_source=vision.GcsSource(uri=gcs_source_uri),
        mime_type='application/pdf'
    )

    # Cấu hình đầu ra (JSON RAW)
    # batch_size=3: mỗi file JSON chứa 3 trang → DeepSeek/LLM dễ đọc
    output_config = vision.OutputConfig(
        gcs_destination=vision.GcsDestination(uri=gcs_destination_uri),
        batch_size=3
    )

    # Language hints cho Myanmar + Pāḷi + English
    image_context = vision.ImageContext(
        language_hints=['my', 'pi', 'en']
    )

    async_request = vision.AsyncAnnotateFileRequest(
        features=[feature],
        input_config=input_config,
        output_config=output_config,
        image_context=image_context,
    )

    print(f"🚀 Bắt đầu OCR async...")
    print(f"   Input:  {gcs_source_uri}")
    print(f"   Output: {gcs_destination_uri}")
    
    operation = client.async_batch_annotate_files(requests=[async_request])
    
    print(f"⏳ Đang đợi Cloud Vision xử lý toàn bộ PDF...")
    print(f"   Operation: {operation.operation.name}")
    
    result = operation.result(timeout=420)  # timeout 7 phút
    
    print(f"✅ OCR hoàn thành!")
    return result


def download_results(gcs_output_prefix, local_dir):
    """
    Download tất cả file JSON output từ GCS về local.
    KHÔNG gộp — giữ nguyên từng file.
    """
    storage_client = storage.Client()
    
    # Parse gs://bucket/prefix
    parts = gcs_output_prefix.replace('gs://', '').split('/', 1)
    bucket_name = parts[0]
    prefix = parts[1] if len(parts) > 1 else ''
    
    bucket = storage_client.bucket(bucket_name)
    blobs = list(bucket.list_blobs(prefix=prefix))
    json_blobs = [b for b in blobs if b.name.endswith('.json')]
    
    if not json_blobs:
        print("⚠️  Không tìm thấy file JSON output nào!")
        return
    
    os.makedirs(local_dir, exist_ok=True)
    
    for blob in json_blobs:
        filename = os.path.basename(blob.name)
        local_path = os.path.join(local_dir, filename)
        blob.download_to_filename(local_path)
        print(f"   ✅ {filename} ({blob.size:,} bytes)")
    
    print(f"📥 Downloaded {len(json_blobs)} JSON files → {local_dir}")


if __name__ == '__main__':
    import sys
    
    if len(sys.argv) < 2:
        print("Usage: python ocr_pdf_to_raw.py <local_pdf_path> [bucket_name] [local_output_dir]")
        print("Example: python ocr_pdf_to_raw.py VDP-181-319.pdf zen-ocr-pdf ocr/raw/")
        sys.exit(1)
    
    local_pdf_path = sys.argv[1]
    bucket_name = sys.argv[2] if len(sys.argv) > 2 else 'zen-ocr-pdf'
    local_output_dir = sys.argv[3] if len(sys.argv) > 3 else 'ocr/raw/'
    
    pdf_name = os.path.splitext(os.path.basename(local_pdf_path))[0]
    
    # Bước 1: Upload PDF lên GCS
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(f"input/{os.path.basename(local_pdf_path)}")
    blob.upload_from_filename(local_pdf_path)
    
    gcs_source_uri = f"gs://{bucket_name}/input/{os.path.basename(local_pdf_path)}"
    gcs_dest_uri = f"gs://{bucket_name}/output/{pdf_name}/"
    
    print(f"✅ Uploaded: {gcs_source_uri}")
    
    # Bước 2: OCR
    ocr_pdf_to_raw_json(gcs_source_uri, gcs_dest_uri)
    
    # Bước 3: Download
    download_results(gcs_dest_uri, local_output_dir)
    
    print(f"\n🎉 Done! JSON files saved to {local_output_dir}")
```

### Chạy

```bash
cd /home/tuan-nguyen/.openclaw/workspace
python3 scripts/ocr_pdf_to_raw.py \
  "001-vi-dieu-phap-giang-giai/VDP-181-319.pdf" \
  "zen-ocr-pdf" \
  "001-vi-dieu-phap-giang-giai/ocr/raw/"
```

### Thời gian chờ ước tính

| Số trang | Số file JSON (batch_size=3) | Thời gian |
|----------|---------------------------|-----------|
| ~50 trang | 17 files | 2-3 phút |
| ~100 trang | 34 files | 3-5 phút |
| ~139 trang (VDP 181-319) | 47 files | 5-7 phút |
| ~300 trang | 100 files | 10-15 phút |

---

## 📂 Cấu Trúc Thư Mục

```
001-vi-dieu-phap-giang-giai/
  ├── Vi Dieu Phap Giang Giai.pdf     ← PDF gốc (319 trang)
  ├── VDP-181-319.pdf                 ← PDF đã cắt (trang 181-319, 139 trang)
  ├── ocr/
  │   └── raw/
  │       ├── output-1-to-5.json      ← Cloud Vision output (trang 1-5)
  │       ├── output-6-to-10.json     ← Cloud Vision output (trang 6-10)
  │       ├── output-11-to-15.json    
  │       ├── ...
  │       └── output-136-to-139.json  ← Trang cuối (4 trang còn lại)
  │
  │   ⚠️ KHÔNG gộp — giữ nguyên từng file 3 trang
  │
  └── img/                            ← (cũ) Không dùng nữa
```

**GCS structure:**
```
gs://zen-ocr-pdf/
  ├── input/
  │   └── VDP-181-319.pdf             ← PDF input
  └── output/
      └── VDP-181-319/
          ├── output-1-to-5.json
          ├── output-6-to-10.json
          ├── ...
          └── output-136-to-139.json
```

---

## 🔑 Language Hints

```python
image_context = vision.ImageContext(
    language_hints=['my', 'pi', 'en']
)
```

| Mã | Ngôn ngữ |
|----|----------|
| `my` | Myanmar |
| `pi` | Pāḷi |
| `en` | English (số, ký hiệu Latin) |

---

## ⚠️ Lưu Ý Quan Trọng

### Ưu điểm Async PDF

- ✅ **Không cần tách ảnh** — Gửi thẳng PDF, không cần `pdfseparate` hay chuyển sang jpg
- ✅ **Tự động batch** — Cloud Vision tự xử lý từng trang, song song
- ✅ **batch_size=3** — Mỗi file JSON 5 trang, LLM (DeepSeek) đọc được, không bị quá nặng
- ✅ **Output có cấu trúc** — JSON chứa block → paragraph → word → symbol + confidence
- ✅ **Giữ nguyên file** — Không gộp, dễ truy vết từng batch, dễ xử lý lại từng phần

### Hạn chế

- ⚠️ **PDF ≤ 2000 trang** — Giới hạn của Cloud Vision async API
- ⚠️ **PDF ≤ 2GB** — Giới hạn kích thước file
- ⚠️ **Phải ở GCS** — File input phải nằm trong Google Cloud Storage, không gửi trực tiếp local
- ⚠️ **Chi phí** — Tính theo số trang × API pricing (xem [GCP pricing](https://cloud.google.com/vision/pricing))
- ⚠️ **Timeout** — Script đặt timeout=420s, nếu PDF quá dày có thể cần tăng

### Spacing Myanmar

Cloud Vision có thể thêm/bớt khoảng trắng giữa các âm tiết Myanmar. Cần script hậu xử lý spacing sau khi có JSON.

---

## 📊 Cấu Trúc JSON Output (mỗi file = 5 trang)

```json
{
  "responses": [
    {
      "fullTextAnnotation": {
        "text": "... toàn bộ text của trang 1 ...",
        "pages": [{
          "blocks": [{
            "paragraphs": [{
              "words": [{
                "symbols": [{
                  "text": "က",
                  "confidence": 0.98
                }],
                "confidence": 0.95
              }]
            }],
            "blockType": "TEXT",
            "confidence": 0.92
          }]
        }]
      }
    },
    {
      "fullTextAnnotation": {
        "text": "... toàn bộ text của trang 2 ...",
        ...
      }
    },
    ... (3 responses cho 3 trang)
  ]
}
```

### Trường quan trọng

| Trường | Mô tả |
|--------|-------|
| `fullTextAnnotation.text` | Toàn bộ text của 1 trang (plain text) |
| `blocks[].confidence` | Độ tin cậy của block |
| `symbols[].text` | Ký tự đơn lẻ |
| `symbols[].confidence` | Độ tin cậy của từng ký tự |

---

## 🔗 Liên Kết Nội Bộ

- [[../quy-trinh/03-Hieu-Dinh]] — Quy trình hiệu đính từ JSON raw
- [[OCR-Checklist]] — Checklist các lỗi OCR phổ biến

---

**Tags:** #quy-trinh #ocr #google-cloud-vision #async #pdf #myanmar #pali #tam-bao
