# Quy Trình 01A — Điều Phối Editor Hiệu Đính OCR

> **Dành cho Architect** — thiết lập & giám sát cron job điều phối editor
> Cập nhật: 2026-06-09 | Phiên bản: **4.2** (cache-hit optimized + rules A→H + notes tối ưu)

---

## I. NGUYÊN LÝ CỐT LÕI

```
┌─────────────────────────────────────────────────────────────┐
│                  KIẾN TRÚC TỐI ƯU V4.2                       │
│                                                              │
│  EDITOR AI (2 LLM CALLS — ~23K TOKEN)                        │
│  ├─ PHASE 1 (1 call): exec python → chọn batch + source     │
│  │                    + đọc GUIDE (03A) + SQLite patterns    │
│  └─ PHASE 2 (1 call): write+write+exec, TẤT CẢ CÙNG LÚC    │
│                       KHÔNG tạo text report riêng            │
│                                                              │
│  TOKEN: 434K → ~32K → ~23K (GIẢM 95%)                       │
│  lightContext: true → bỏ system prompt files                 │
│  Guide (03A) in từ exec → single source of truth             │
│  Notes: chỉ ghi lỗi thực sự (không format) → tiết kiệm output│
└─────────────────────────────────────────────────────────────┘
```

### 🔥 Cache Hit Strategy (V4.2)

Prompt cron **không chứa rules** — rules nằm trong 03A guide, được Phase 1 đọc và in ra qua exec output. Điều này:
- **Single source of truth** — chỉ cần sửa 03A, không cần đồng bộ prompt
- **Không phá cache** — guide được in qua exec output (variable), không nằm trong static prefix
- **Vẫn 2 API calls** — guide + source + patterns được in cùng 1 lần exec

```
┌──────────────────────────────────────────┐
│  CRON MESSAGE (STATIC — CACHE HIT 100%)  │
│  ┌────────────────────────────────────┐  │
│  │ Phase 1 script + Phase 2 hướng dẫn │  │
│  │ → KHÔNG chứa rules (rules ở 03A)   │  │
│  │ → Rất ngắn (~3K)                   │  │
│  └────────────────────────────────────┘  │
│         ↓ Turn 1: exec python            │
│  ┌────────────────────────────────────┐  │
│  │ EXEC OUTPUT (VARIABLE)              │  │
│  │ → GUIDE (~10K) + source + patterns  │  │
│  │ → Token mới mỗi batch               │  │
│  └────────────────────────────────────┘  │
│         ↓ Turn 2: write + write + exec   │
└──────────────────────────────────────────┘
```

| Lần chạy | Turn 1 input | Turn 2 input mới | Tổng token mới |
|----------|-------------|-----------------|---------------|
| Lần 1 | ~3K (prompt gọn) | ~10K (guide) + ~10K (source+patterns) = ~20K | ~23K |
| Lần 2+ | ~0 (cache hit) | ~10K (guide) + ~10K (source) = ~20K | ~20K |

> 💡 **Guide ~10K chars nhưng được cache ở prompt prefix?** Không — guide nằm trong exec output (variable). Nhưng vì guide giống hệt mỗi lần, provider có thể cache ở tầng infrastructure. Quan trọng nhất: **single source of truth**, không duplicate rules.

### Tại sao 11 calls → 2 calls?

| Trước (V3.1) | Sau (V4.1) | Lý do |
|-------------|-----------|-------|
| "SAU KHI CHẠY → LÀM TIẾP" → làm tuần tự | "GỌI TẤT CẢ TOOL CÙNG LÚC" → song song | Editor không còn chờ từng bước |
| exec → chờ → write → chờ → exec → chờ... (11 calls) | Phase 1 (exec) → Phase 2 (write+write+exec, 1 call) | Context không bị gửi lại 11 lần |
| Context 14K→20K→34K→42K→49K (tích lũy) | Context 10K→14K (chỉ 2 lần) | Token waste giảm 93% |
| Workspace files trong context (~2-5K) | lightContext=true → bỏ | Prompt inline đã đủ |

### 🐛 Bug V4.0 → fix V4.1

| V4.0 | Vấn đề | V4.1 |
|------|--------|------|
| Phase 2 có `[TEXT] Báo cáo...` | Model crash 4 lần sau tool results (cố sinh text thêm) → cron restart → +295K token lãng phí | Bỏ `[TEXT]` — tool results tự announce qua cron delivery |
| `output-183-to-185`: 32K (hợp lệ) + 295K (retry) = 327K | Net batch done trong 2 calls, nhưng crash gây retry | Chỉ 2 calls, ~32K, không retry |

---

## II. 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-vec DB khi Checklist thay đổi      │
│  • QA hậu kiểm (script apply-marks.py)               │
└──────────────────────────────────────────────────────┘

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

---

## III. THIẾT LẬP CRON JOBS

### Cron 1: `ocr-processor-v4` (Editor — mỗi 15 phút, 2 LLM calls)

```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/003-tang-chi-bo-giang-giai"
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.
   KHÔNG gọi tuần tự (gọi tool → chờ kết quả → gọi tool tiếp).

⚠️ Sau khi gọi tool, thêm 1 DÒNG TEXT NGẮN để xác nhận.
   Ví dụ: "Đã hiệu đính xong {BATCH}: N lỗi."

⚠️ 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---) — đây là toàn bộ quy tắc hiệu đính
   → Đọc SOURCE (giữa ---SOURCE_START--- và ---SOURCE_END---) — ĐÂY LÀ VĂN BẢN CẦN HIỆU ĐÍNH. CHỈ LÀM VIỆC VỚI TEXT NÀY.
   → Đọc PATTERNS (giữa ---PATTERNS_START--- và ---PATTERNS_END---)
   → Áp dụng GUIDE + patterns để HIỆU ĐÍNH (sửa lỗi OCR, gộp dòng, format) — KHÔNG DỊCH, KHÔNG VIẾT LẠI BẰNG PALI

   → 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/003-tang-chi-bo-giang-giai/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/003-tang-chi-bo-giang-giai/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 |
     |---|-------|-----------|-------------|------|
     | 1 | X | `chữ_sai` | `chữ_đúng` | Pali/Trợ từ/Số đếm/Nhiễu |
     ⚠️ KHÔNG ghi: gộp dòng, thêm dòng trống, bold ** (thuần format, không đổi ký tự)

   [TOOL 3] exec python (thay BATCH bằng tên thật từ output Phase 1):
     import json
     from datetime import datetime, timezone, timedelta
     PROJ = "/home/tuan-nguyen/.openclaw/workspace/003-tang-chi-bo-giang-giai"
     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 hoàn thành.' \
  --timeout-seconds 600 \
  --delivery "announce,telegram,412242443"
```

**Cấu trúc payload quan trọng:**
```json
{
  "lightContext": true,
  "timeoutSeconds": 600,
  "model": "openrouter/google/gemini-3-flash-preview",
  "thinking": "off"
}
```

> ⚠️ **`thinking: "off"` là bắt buộc** — Google API mặc định bật reasoning mode, khiến output tokens tăng 2-3 lần (9K→4.5K sau khi tắt) mà không cải thiện chất lượng hiệu đính. Tiết kiệm ~$0.017/batch.

---

### 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/003-tang-chi-bo-giang-giai"
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 stuck / pipeline dead
- in_progress > 20min + no output → STUCK → reset
- in_progress == 0 + pending > 0 + idle > 20min → PIPELINE DEAD

### 3. Actions
- STUCK: reset → pending
- PIPELINE DEAD: enable processor
- ALL DONE: disable processor cron + self-disable ocr-monitor

### 4. Self-disable khi ALL DONE
When done == total_batches (in data["batches"]) and pending == 0 and in_progress == 0:
  1. Use cron tool: find job "ocr-processor-v4" → update enabled=false
  2. Use cron tool: find job "ocr-monitor" → update enabled=false
  3. Report "✅ Pipeline complete — all crons disabled."

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

> ⚠️ Cron monitor hiện đang disabled — chỉ enable khi pipeline đang chạy.

---

## IV. SQLITE-VEC OCR CHECKLIST

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

| Category | Mô tả | Số lượng |
|----------|-------|----------|
| 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 khi cập nhật Checklist:
```bash
cd /home/tuan-nguyen/.openclaw/workspace && python3 << 'PYEOF'
import re, sqlite3, json, 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
```

---

## V. GIÁM SÁT CHI PHÍ

| Phiên bản | Token/batch | 224 batch | LLM calls/batch | Chi phí | Ghi chú |
|-----------|------------|-----------|-----------------|---------|--------|
| V2.2 (10 steps) | 2.4M | 540M | ~20 | ~$112 | |
| V3.0 (pre-process) | 71K | 16M | ~10 | ~$3.50 | |
| V3.1 (editor-only) | ~434K | ~97M | ~11 | ~$20 | Tuần tự 11 calls |
| V4.0 (parallel) | ~32K + retry bug | ~7M | 2 | ~$2.50 | Crash text report |
| V4.1 (fixed) | ~32K | ~7M | 2 | ~$2.50 | Ổn định, không crash |
| **V4.2 (guide in exec)** | **~23K** | **~5.2M** | **2** | **~$1.80** | **Single source of truth + notes tối ưu** |

**Phân tích session thực tế:**

| Session | Version | Batch | Calls | Token | Cost |
|---------|---------|-------|-------|-------|------|
| `c6f89acd` | V3.1 | output-16-to-20 (5p) | 11 | 434K | $0.18 |
| `f9ec755e` | V4.0 | output-183-to-185 (3p) | 2 + retry | 32K + 295K waste | $0.17 |
| Dự kiến V4.2 | V4.2 | any | **2** | **~10K (sau cache)** | **~$0.005** |

**Chi tiết V4.0 session `f9ec755e` (attempt 1 — thành công):**

| Call | Input | Output | Mục đích |
|------|-------|--------|----------|
| 1 | 10,022 | 852 | PHASE 1: exec python (batch + source + patterns) |
| 2 | 14,082 | 7,369 | PHASE 2: write(edited 5,438B) + write(notes 692B) + exec(progress) — TẤT CẢ CÙNG LÚC |
| **Tổng** | **24,104** | **8,221** | **2 calls, $0.037** ✅ |
| Lines 11-14 | — | — | ❌ Crash 4×: model cố sinh text report sau tool results |
| Line 16+ | 295K | — | 🔄 Cron restart: chạy lại toàn bộ (lãng phí) |

---

## VI. TỔNG KẾT

```
V4.2 — GUIDE IN EXEC + SINGLE SOURCE OF TRUTH + NO TEXT REPORT

  EDITOR AI (2 LLM CALLS)               ARCHITECT
  ┌──────────────────────────┐         ┌──────────────┐
  │ PHASE 1: exec python     │         │ Giám sát 30ph │
  │  • Chọn batch + source   │         │ QA hậu kiểm   │
  │  • Đọc GUIDE (03A)       │         │ Cập nhật DB   │
  │  • SQLite patterns       │         │ Fix sự cố     │
  ├──────────────────────────┤         └──────────────┘
  │ PHASE 2: 1 RESPONSE      │
  │  • write edited file     │
  │  • write notes (chỉ lỗi) │
  │  • exec update progress  │
  │  ⚠️ KHÔNG text report!   │
  │ GỌI TẤT CẢ CÙNG LÚC      │
  │ ~23K token, ~$0.008/batch│
  └──────────────────────────┘
```

> **Thay đổi chính V4.1→V4.2:**
> 1. **Single source of truth**: Rules không còn inline trong prompt → chỉ có trong 03A guide
> 2. **Guide in exec**: Phase 1 đọc 03A và in ra cùng source + patterns → editor đọc tất cả từ exec output
> 3. **Prompt gọn hơn 80%**: ~12K → ~3K (chỉ còn script + hướng dẫn gọi tool)
> 4. **Notes chỉ ghi lỗi thực sự** → tiết kiệm ~40% output token
> 5. **Không cần đồng bộ prompt khi sửa rules** — chỉ cần update 03A
>
> **Thay đổi chính từ V3.1→V4.1:**
> 1. Prompt yêu cầu gọi TẤT CẢ tool trong 1 response (thay vì tuần tự)
> 2. `lightContext: true` — bỏ AGENTS/SOUL/USER/TOOLS.md khỏi context
> 3. 11 LLM calls → 2 LLM calls (giảm 93% token: 434K → 32K)
> 4. Bỏ `[TEXT]` report — tránh crash sau tool results (bug V4.0 đã fix)
> 5. Cron interval: 5min → 15min
> 6. Cron name: `ocr-processor-v3` → `ocr-processor-v4`

> 📖 **File liên quan:**
> - `scripts/apply-marks.py` — Script QA hậu kiểm: bọc `<mark>` vào thuật ngữ đã sửa trong file edited
> - `scripts/merge-lines.py` — Script gộp dòng bị OCR cắt ngang (merge Myanmar lines by sentence markers)
> - `data/ocr-checklist-vec.db` — SQLite-vec database
> - `progress-ocr.json` — Tiến độ pipeline
