# Quy Trình Tổng Hợp — OCR Pipeline: Trích Xuất → Hiệu Đính → Highlight

> **Dành cho Architect** — toàn bộ pipeline từ PDF scan đến file đã highlight
> Tổng hợp: 2026-06-13 | Từ 04-OCR-Pipeline + 01A-Dieu-Phoi-Editor-OCR

---

## 🗺️ Tổng Quan Pipeline

```
┌─────────────────────────────────────────────────────────────────────┐
│                    OCR PIPELINE TOÀN TRÌNH                           │
│                                                                      │
│  PHASE 1: TRÍCH XUẤT          PHASE 2: HIỆU ĐÍNH     PHASE 3: QA   │
│  ┌──────────────────┐    ┌──────────────────────┐   ┌────────────┐  │
│  │ PDF scan          │    │ Editor AI            │   │ Architect  │  │
│  │  ↓ (preprocess)   │    │ (cron, 2 LLM calls)  │   │ QA         │  │
│  │ Cloud Vision OCR  │ →  │  ↓                   │ → │ ↓          │  │
│  │  ↓                │    │ edited/gemini-flash/  │   │ apply-marks│  │
│  │ JSON raw           │    │ edited-notes/        │   │ .py        │  │
│  │  ↓ V7 script      │    │ (bảng sửa lỗi)       │   │ ↓          │  │
│  │ extracted/ (.md)  │    └──────────────────────┘   │ <mark>     │  │
│  └──────────────────┘                                └────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
```

| Phase | Ai làm | Công cụ | Input | Output |
|-------|--------|---------|-------|--------|
| **1. Trích xuất** | Architect (thủ công) | Cloud Vision API + V7 script | PDF scan | `extracted/*.md` (sạch, layout đẹp) |
| **2. Hiệu đính** | Editor Agent (cron tự động) | Gemini Flash, 2 LLM calls | `extracted/*.md` | `edited/*.md` + `edited-notes/*-notes.md` |
| **3. Highlight** | Architect (1 lệnh) | `apply-marks.py` | `edited/` + `edited-notes/` | `edited/*.md` (có `<mark>`) |

---

# PHASE 1: TRÍCH XUẤT OCR (PDF → Markdown sạch)

## Bước 0: Preprocess Ảnh Scan (tùy chọn)

> **Khi nào cần:** Giấy mỏng, thấu quang, chữ mờ, nhiều đốm nhiễu.
> **Không cần nếu:** PDF scan từ sách in chất lượng tốt.

### Script

```bash
# Trích xuất PDF → PNG
pdftoppm -r 300 -png input.pdf raw_pages/page

# Xử lý adaptive threshold + morphology
python3 preprocess_all.py

# Gộp lại thành PDF
img2pdf cleaned/page-*.png -o output-clean.pdf
```

### Tham số (preprocess-config.yaml)

```yaml
preprocessing:
  bilateral_d: 9
  bilateral_sigmaColor: 75
  bilateral_sigmaSpace: 75
  adaptive_threshold:
    method: ADAPTIVE_THRESH_GAUSSIAN_C
    block_size: 21
    C: 25
  morphology:
    kernel: [3, 3]
    mode: MORPH_CLOSE
  output:
    dpi: 300
    format: png
```

| Tham số | Tăng lên | Giảm xuống |
|---------|----------|------------|
| **C** | Chữ mờ → trắng, chữ đậm dễ đứt nét | Giữ nét chữ, chữ mờ còn |
| **block_size** | Giữ nét chữ to, bỏ sót chữ mờ vùng tối | Nhạy chi tiết, dễ sinh nhiễu |
| **morph kernel** | Diệt đốm mạnh, dễ mất dấu câu nhỏ | — |

---

## Bước 1: OCR với Cloud Vision API

### Thiết lập

- **Key file:** `old/google_service_account.json`
- **Project ID:** `zen-490314`
- **Service Account:** `openclaw-service@zen-490314.iam.gserviceaccount.com`
- **Quyền:** `roles/storage.objectAdmin` + `roles/visionai.user`

### Chạy OCR

```bash
cd /home/tuan-nguyen/.openclaw/workspace
export GOOGLE_APPLICATION_CREDENTIALS="old/google_service_account.json"

python3 scripts/ocr_pdf_to_raw.py \
  "010-pali-thaykha/pdf/input.pdf" \
  "zen-ocr-pdf" \
  "010-pali-thaykha/ocr/raw/"
```

> Output: JSON files trong `ocr/raw/` — 3-5 trang/file

---

## Bước 2: JSON → Markdown + Cleanup (V7) ⭐

> **1 bước duy nhất** — Y-bucket layout + Header/Footer cleanup tích hợp.

### Pipeline nội bộ V7

```
JSON → [Detect Header/Footer bằng tọa độ block]
     → [Extract paragraphs, Y-bucket sort]
     → [Filter fragments: header zone]
     → Markdown sạch (layout đẹp + header removed)
```

### Cách chạy

```bash
cd /home/tuan-nguyen/.openclaw/workspace

# Chạy bình thường
python3 obsidian/scripts/json_to_markdown_v7.py \
  "010-pali-thaykha/ocr/raw/" \
  "010-pali-thaykha/extracted/"

# Debug (hiện fragment/bucket count + log cleanup)
python3 obsidian/scripts/json_to_markdown_v7.py \
  "010-pali-thaykha/ocr/raw/" \
  "010-pali-thaykha/extracted/" \
  --debug

# Dry-run
python3 obsidian/scripts/json_to_markdown_v7.py \
  "010-pali-thaykha/ocr/raw/" \
  "010-pali-thaykha/extracted/" \
  --dry-run
```

### Tham số tinh chỉnh (trong script)

| Tham số | Default | Ý nghĩa |
|---------|---------|---------|
| `BUCKET_SIZE` | `0.022` | Ngưỡng Y (normalized) để gộp vào cùng dòng |
| `COLUMN_GAP_THRESHOLD` | `0.15` | Khoảng cách X → 4 spaces (cột) |
| `WORD_GAP_THRESHOLD` | `0.03` | Khoảng cách X → 2 spaces (từ) |
| `HEADER_DETECT_Y` | `0.15` | Block có y_min ≤ đây → header zone candidate |
| `HEADER_CLEAN_Y` | `0.15` | Fragment có y_min ≤ đây → thực sự xóa |
| `FOOTER_Y_MIN` | `0.82` | Block có y_max > đây → footer zone (không filter, chỉ detect) |

### Output

- Mỗi file JSON → 1 file `.md` (VD: `output-1-to-3.md`, `output-4-to-6.md`, ...)
- Mỗi trang bắt đầu bằng `## PAGE X`
- Tự động tạo file merged `Pali-Taykha-full.md`
- Layout Y-bucket: đúng thứ tự đọc, spacing chuẩn, header đã xóa

---

# PHASE 2: HIỆU ĐÍNH OCR (Markdown sạch → Edited)

## Kiến trúc

```
┌──────────────────────────────────────────────────────┐
│                 ARCHITECT (giám sát)                  │
│  • 1 cron job ocr-processor (every 15min)             │
│  • 1 cron job ocr-monitor (every 30min)               │
│  • Cập nhật SQLite patterns khi Checklist thay đổi    │
│  • QA hậu kiểm (Phase 3)                             │
└──────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────┐
│         EDITOR AI (Gemini Flash, 2 LLM CALLS)         │
│  ┌──────────────────────────────────────────┐        │
│  │ PHASE 1: exec python                      │        │
│  │   • Chọn batch pending                    │        │
│  │   • Đọc source từ extracted/              │        │
│  │   • Đọc GUIDE (03A) + SQLite patterns     │        │
│  │   → In tất cả vào exec output             │        │
│  └──────────────────────────────────────────┘        │
│  ┌──────────────────────────────────────────┐        │
│  │ PHASE 2: ALL TOOLS IN 1 RESPONSE          │        │
│  │   • write → edited/gemini-flash/{B}.md    │        │
│  │   • write → edited-notes/{B}-notes.md     │        │
│  │   • exec → update progress-ocr.json       │        │
│  │   ⚠️ KHÔNG text report riêng!             │        │
│  └──────────────────────────────────────────┘        │
│  ⚡ ~23K token, ~$0.008/batch                        │
└──────────────────────────────────────────────────────┘
```

## Thiết lập Cron Jobs

### Cron 1: `ocr-processor` (Editor — mỗi 15 phút)

```bash
openclaw cron add \
  --name ocr-processor-v4 \
  --every 15m \
  --session isolated \
  --agent editor \
  --model openrouter/google/gemini-3-flash-preview \
  --light-context true \
  --thinking off \
  --message 'Bạn là Editor Agent hiệu đính OCR Myanmar — TỐI ƯU 2 LLM CALLS.

═══════════════════
QUY TẮC — XEM GUIDE (in từ Phase 1)
═══════════════════

→ Toàn bộ quy tắc hiệu đính nằm trong file 03A-Hieu-Dinh-Myanmar-OCR.md
→ Phase 1 sẽ in nội dung guide ra cùng với source + patterns
→ Editor đọc guide từ Phase 1 output, không cần rules inline trong prompt này

═══════════════════
PHASE 1: CHẠY SCRIPT NÀY (1 LLM call)
═══════════════════

```python
import json, os, sqlite3, glob
from datetime import datetime, timezone, timedelta
PROJ = "/home/tuan-nguyen/.openclaw/workspace/010-pali-thaykha"
tz = timezone(timedelta(hours=7))

with open(f"{PROJ}/guide/03A-Hieu-Dinh-Myanmar-OCR.md") as f: guide = f.read()

with open(f"{PROJ}/progress-ocr.json") as f: data = json.load(f)
edited = {os.path.basename(x).replace(".md","") for x in glob.glob(f"{PROJ}/edited/gemini-flash/*.md")}
notes = {os.path.basename(x).replace("-notes.md","") for x in glob.glob(f"{PROJ}/edited-notes/gemini-flash/*.md")}
for b in data["batches"]:
    if b["status"] == "in_progress":
        bn = b["name"]
        if bn in edited and bn in notes:
            b["status"] = "done"; b["completed_at"] = datetime.now(tz).isoformat()
            print("RECOVER:" + bn)
        else:
            started = datetime.fromisoformat(b.get("started_at","2000-01-01T00:00:00+07"))
            if (datetime.now(tz)-started).total_seconds() > 900:
                b["status"] = "pending"; print("STUCK:" + bn)
            else: print("SKIP:" + bn); exit()
        with open(f"{PROJ}/progress-ocr.json","w") as f: json.dump(data,f,indent=2,ensure_ascii=False)
target = None
for b in data["batches"]:
    bn = b["name"]
    if b["status"] == "pending" or (bn not in edited or bn not in notes):
        target = b; break
if target is None:
    print("ALL_DONE")
    exit()
target["status"] = "in_progress"; target["started_at"] = datetime.now(tz).isoformat()
BATCH = target["name"]; print("BATCH:" + BATCH)
with open(f"{PROJ}/progress-ocr.json","w") as f: json.dump(data,f,indent=2,ensure_ascii=False)

# Đọc source TRỰC TIẾP từ extracted
with open(f"{PROJ}/extracted/{BATCH}.md") as f: source = f.read()

conn = sqlite3.connect("/home/tuan-nguyen/.openclaw/workspace/data/ocr-checklist-vec.db")
cur = conn.cursor()
cur.execute("SELECT find_text, replace_text, note FROM patterns WHERE is_mechanical=1 ORDER BY id LIMIT 30")
mech = cur.fetchall()
cur.execute("SELECT find_text, replace_text, note, category FROM patterns WHERE is_mechanical=0 ORDER BY id LIMIT 60")
sem = cur.fetchall()
conn.close()
print(f"GUIDE:{len(guide)} chars")
print(f"PATTERNS:{len(mech)} mech + {len(sem)} semantic")
print("---GUIDE_START---")
print(guide)
print("---GUIDE_END---")
print("---SOURCE_START---")
print(source)
print("---SOURCE_END---")
print("---PATTERNS_START---")
for f_text, r_text, note, cat in sem:
    print(f"  [{cat}] {f_text} -> {r_text} | {note}")
print("---PATTERNS_END---")
```

═══════════════════
PHASE 2: 1 RESPONSE — GỌI TẤT CẢ TOOL CÙNG LÚC
═══════════════════

⚠️ PHASE NÀY PHẢI HOÀN THÀNH TRONG ĐÚNG 1 RESPONSE.
   GỌI TẤT CẢ TOOL SONG SONG TRONG CÙNG 1 LƯỢT.

⚠️ Nếu Phase 1 in "ALL_DONE": KHÔNG làm gì hết.

Khi Phase 1 in "BATCH:{tên_batch}":
   → Đọc GUIDE (giữa ---GUIDE_START--- và ---GUIDE_END---)
   → Đọc SOURCE (giữa ---SOURCE_START--- và ---SOURCE_END---)
   → Đọc PATTERNS (giữa ---PATTERNS_START--- và ---PATTERNS_END---)
   → Áp dụng GUIDE + patterns để HIỆU ĐÍNH — KHÔNG DỊCH

   → SAU ĐÓ, GỌI ĐỒNG THỜI TẤT CẢ CÁC TOOL SAU TRONG 1 RESPONSE:

   [TOOL 1] write:
     path: /home/tuan-nguyen/.openclaw/workspace/010-pali-thaykha/edited/gemini-flash/{BATCH}.md
     content: toàn bộ file đã hiệu đính (ngắt đoạn + bold Pāli, GIỮ NGUYÊN TIẾNG MYANMAR)

   [TOOL 2] write:
     path: /home/tuan-nguyen/.openclaw/workspace/010-pali-thaykha/edited-notes/gemini-flash/{BATCH}-notes.md
     content: bảng notes markdown (CHỈ ghi lỗi thay đổi ký tự, không ghi formatting)
     | # | Trang | Lỗi (OCR) | Đã sửa thành | Loại |
     |---|-------|-----------|-------------|------|

   [TOOL 3] exec python:
     import json
     from datetime import datetime, timezone, timedelta
     PROJ = "/home/tuan-nguyen/.openclaw/workspace/010-pali-thaykha"
     BATCH = "{BATCH}"
     with open(f"{PROJ}/progress-ocr.json") as f: data = json.load(f)
     for b in data["batches"]:
         if b["name"] == BATCH: b["status"] = "done"; b["completed_at"] = datetime.now(timezone(timedelta(hours=7))).isoformat(); break
     with open(f"{PROJ}/progress-ocr.json","w") as f: json.dump(data,f,indent=2,ensure_ascii=False)
     print("OK")

   → SAU TOOL, thêm 1 dòng text ngắn xác nhận.' \
  --timeout-seconds 600 \
  --delivery "announce,telegram,412242443"
```

### Cron 2: `ocr-monitor` (Architect — mỗi 30 phút)

```bash
openclaw cron add \
  --name ocr-monitor \
  --every 30m \
  --session isolated \
  --agent architect \
  --model deepseek/deepseek-v4-flash \
  --light-context true \
  --message '## OCR Monitor

### 1. Check progress
```python
import json, glob
PROJ = "/home/tuan-nguyen/.openclaw/workspace/010-pali-thaykha"
with open(f"{PROJ}/progress-ocr.json") as f: data = json.load(f)
from collections import Counter
c = Counter(b["status"] for b in data["batches"])
n = len(glob.glob(f"{PROJ}/edited-notes/gemini-flash/*.md"))
print(f"Done={c.get(\"done\",0)} InProg={c.get(\"in_progress\",0)} Pend={c.get(\"pending\",0)} Notes={n}")
```

### 2. Detect issues
- in_progress > 20min + no output → STUCK → reset to pending
- in_progress == 0 + pending > 0 + idle > 20min → PIPELINE DEAD

### 3. Self-disable khi ALL DONE
When done == total_batches:
  1. cron: find "ocr-processor-v4" → update enabled=false
  2. cron: find "ocr-monitor" → update enabled=false
  3. Report "✅ Pipeline complete — all crons disabled."

### 4. Report 1 dòng' \
  --timeout-seconds 120 \
  --delivery "announce,telegram,412242443"
```

> ⚠️ Cron monitor chỉ enable khi pipeline đang chạy. Disable khi idle.

---

## Cấu trúc thư mục dự án

```
010-pali-thaykha/
├── pdf/                    # PDF gốc
├── ocr/raw/                # JSON output từ Cloud Vision
├── extracted/              # Markdown sạch (V7 output)
├── edited/
│   └── gemini-flash/       # File đã hiệu đính (Editor)
├── edited-notes/
│   └── gemini-flash/       # Bảng notes sửa lỗi (Editor)
├── guide/
│   └── 03A-Hieu-Dinh-Myanmar-OCR.md   # Quy tắc hiệu đính
├── progress-ocr.json       # Tiến độ pipeline
├── preprocess_all.py       # Script preprocess
└── preprocess-config.yaml  # Cấu hình preprocess
```

## SQLite Patterns (OCR Checklist)

**Database:** `/home/tuan-nguyen/.openclaw/workspace/data/ocr-checklist-vec.db`

| Category | Mô tả |
|----------|-------|
| Pali | Thuật ngữ Pāli (~80) |
| CoHoc | Lỗi dấu phụ/spacing/ký tự (~180) |
| SaiNghia | Từ sai nghĩa (~30) |
| Khac | Lỗi khác (~20) |

### Rebuild DB khi cập nhật Checklist

```bash
cd /home/tuan-nguyen/.openclaw/workspace && python3 << 'PYEOF'
import re, sqlite3, os
with open('obsidian/huong-dan/OCR-Checklist.md') as f: text = f.read()
patterns = []
for line in text.split('\n'):
    if not line.startswith('|'): continue
    if re.match(r'^\|[- ]+\|', line): continue
    if 'Lỗi' in line or 'STT' in line: continue
    cells = [c.strip() for c in line.split('|')]
    cells = [c for c in cells if c]
    if len(cells) < 3: continue
    mm = []
    for i, c in enumerate(cells):
        if re.search(r'[\u1000-\u109F]', c) and not re.search(r'[a-zA-Z]', c):
            mm.append(i)
    if len(mm) < 2: continue
    fi = mm[-2] if mm[-1]-mm[-2]==1 else mm[-2]
    ri = mm[-1]
    old_t = cells[fi].replace('`','').strip()
    new_t = cells[ri].replace('`','').strip()
    old_t = re.sub(r'\s*\([^)]*\)\s*$','',old_t).strip()
    if not old_t or not new_t or old_t==new_t or '[xóa]' in new_t: continue
    if len(old_t)<2 or re.match(r'^[\d\s\-~%]+$',old_t): continue
    patterns.append({'find':old_t,'replace':new_t})
DB='data/ocr-checklist-vec.db'
if os.path.exists(DB): os.remove(DB)
conn=sqlite3.connect(DB)
conn.execute('CREATE TABLE patterns(id INTEGER PRIMARY KEY, find_text TEXT, replace_text TEXT, note TEXT, category TEXT, is_mechanical INTEGER)')
safe = [p for p in patterns if len(p['find'])>=3 and '**' not in p['find']]
for i,p in enumerate(patterns):
    conn.execute('INSERT INTO patterns VALUES(?,?,?,?,?,?,?)',(i+1,'',p['find'],p['replace'],'','Khac',1 if p in safe else 0))
conn.commit(); conn.close()
print(f'DB: {len(patterns)} patterns ({len(safe)} mechanical)')
PYEOF
```

---

## Giám Sát Chi Phí (Phase 2)

| Phiên bản | Token/batch | LLM calls/batch | Chi phí/batch | Ghi chú |
|-----------|------------|-----------------|---------------|--------|
| V2.2 (10 steps) | 2.4M | ~20 | ~$0.50 | Tuần tự, nhiều bước |
| V3.0 (pre-process) | 71K | ~10 | ~$0.015 | |
| V3.1 (editor-only) | ~434K | ~11 | ~$0.09 | Tuần tự 11 calls |
| V4.0 (parallel) | ~32K | 2 | ~$0.007 | Có bug text report |
| **V4.2 (guide in exec)** | **~23K** | **2** | **~$0.005** | **Single source of truth** |

> **`thinking: "off"` là bắt buộc** — Google API mặc định bật reasoning, tăng output 2-3× mà không cải thiện chất lượng.

---

# PHASE 3: QA HIGHLIGHT (Edited → Marked)

## Mục đích

Sau khi Editor hiệu đính xong toàn bộ batches, chạy `apply-marks.py` để **bọc `<mark>`** vào tất cả các từ đã được sửa trong file edited, dựa trên bảng notes (`edited-notes/`).

```
edited-notes/*-notes.md          edited/*.md
┌──────────────────┐           ┌──────────────────┐
│ | Lỗi | Đã sửa   │           │ ဗုဒ္ဓေါ ဓမ္မံ     │
│ | ဗုဒ္ဓါ  | ဗုဒ္ဓေါ │  ───→   │ <mark>ဗုဒ္ဓေါ</mark> │
└──────────────────┘           └──────────────────┘
```

## Script: `apply-marks.py`

**Vị trí:** `obsidian/scripts/apply-marks.py`

### Cách hoạt động

1. **Parse notes** — đọc cột "Đã sửa thành" từ bảng markdown trong `edited-notes/*-notes.md`
2. **Tách tokens** — split theo khoảng trắng để tìm từng từ riêng lẻ
3. **Match + mark** — tìm tất cả occurrences trong file edited, bọc `<mark>...</mark>`
4. **Deduplicate** — xử lý overlap (ưu tiên match dài hơn), tránh double-wrap

### Cách chạy

```bash
cd /home/tuan-nguyen/.openclaw/workspace/obsidian/scripts

# Dry-run: preview không sửa file
python3 apply-marks.py --all --dry-run

# Chạy thật
python3 apply-marks.py --all

# Single batch
python3 apply-marks.py --batch output-37-to-39 --dry-run
python3 apply-marks.py --batch output-37-to-39
```

### Symlink (quan trọng!)

Script dùng paths relative từ thư mục script:

```
obsidian/scripts/
├── edited/gemini-flash/       → symlink → 010-pali-thaykha/edited/gemini-flash/
├── edited-notes/gemini-flash/ → symlink → 010-pali-thaykha/edited-notes/gemini-flash/
└── _backup/marks/             # Backup tự động trước khi sửa
```

> ⚠️ **Kiểm tra symlink trước khi chạy!** Nếu trỏ nhầm dự án, script sẽ modify sai file.
> ```bash
> ls -la obsidian/scripts/edited/gemini-flash
> ls -la obsidian/scripts/edited-notes/gemini-flash
> ```

### Cập nhật symlink khi đổi dự án

```bash
cd obsidian/scripts
rm edited/gemini-flash
ln -s /home/tuan-nguyen/.openclaw/workspace/<PROJECT>/edited/gemini-flash edited/gemini-flash
rm edited-notes/gemini-flash
ln -s /home/tuan-nguyen/.openclaw/workspace/<PROJECT>/edited-notes/gemini-flash edited-notes/gemini-flash
```

### Output

- Files trong `edited/gemini-flash/` được ghi đè với `<mark>` tags
- Backup tự động lưu vào `_backup/marks/{batch}.md.bak.{timestamp}`
- Console log: số marks, tokens, terms mỗi file

---

# 🎯 QUY TRÌNH VẬN HÀNH (Architect Checklist)

## Khởi động Pipeline

- [ ] **Phase 1:** Chạy OCR + V7 → có `extracted/*.md` + `progress-ocr.json`
- [ ] **Phase 2:** Enable `ocr-processor-v4` + `ocr-monitor` cron jobs
- [ ] **Giám sát:** Theo dõi Telegram notifications từ monitor
- [ ] **Khi ALL DONE:** Monitor tự disable cả 2 cron jobs

## Sau khi Pipeline hoàn tất

- [ ] **Phase 3 — Highlight:** Cập nhật symlink → chạy `apply-marks.py --all`
- [ ] **Kiểm tra:** `grep -c '<mark>' edited/gemini-flash/*.md` để đếm marks
- [ ] **Spot-check:** Mở vài file xem marks có chính xác không
- [ ] **Nếu cần sửa notes:** Sửa `edited-notes/*-notes.md` → chạy lại `apply-marks.py`

## Xử lý sự cố

| Sự cố | Hành động |
|--------|----------|
| Cron processor stuck | Monitor tự reset → pending sau 15ph |
| Pipeline dead | Architect kiểm tra log → restart cron nếu cần |
| Symlink sai dự án | `ls -la` kiểm tra → sửa symlink → restore backup |
| Marks không khớp | Kiểm tra notes format (cột "Đã sửa thành") → chạy lại |
| Lỡ modify sai file | Copy từ `_backup/marks/` timestamp gần nhất |

---

## File Liên Quan

| File | Nội dung |
|------|----------|
| [[04-OCR-Pipeline]] | Chi tiết Phase 1 (trích xuất) + cleanup legacy |
| [[01A-Dieu-Phoi-Editor-OCR]] | Chi tiết Phase 2 (cron + cache strategy) |
| [[03A-Hieu-Dinh-Myanmar-OCR]] | Quy tắc hiệu đính OCR (single source of truth) |
| [[OCR-Checklist]] | 90+ lỗi OCR hệ thống đã tích lũy |
| [[Cloud-Vision-OCR]] | Chi tiết script OCR |
| [[Pali-Taykha-Cleanup-Guide]] | Case study cleanup tọa độ Y |
| [[05-Cron-Quan-Ly]] | Quản lý cron job |
| `scripts/apply-marks.py` | Script highlight Phase 3 |
| `scripts/json_to_markdown_v7.py` | Script JSON→MD Phase 1 |
| `data/ocr-checklist-vec.db` | SQLite patterns cho Phase 2 |
