256 lines
11 KiB
Markdown
256 lines
11 KiB
Markdown
# OCR Sidecar — แผนการ Refactor by CLAUDE
|
||
**ไฟล์:** `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py`
|
||
**วันที่วิเคราะห์:** 2026-06-20
|
||
**GPU ปัจจุบัน:** RTX 5060 Ti 16GB
|
||
**ไฟล์:** `ocr-sidecar-refactor-plan-cluade.md`
|
||
---
|
||
|
||
## สรุปปัญหาที่พบ
|
||
|
||
| # | ปัญหา | ความรุนแรง | หมวด |
|
||
|---|-------|-----------|------|
|
||
| P1 | Hardcoded default API key ใน source code | 🔴 Critical | Security |
|
||
| P2 | `process_ocr` เป็น sync function — block event loop | 🔴 Critical | Performance |
|
||
| P3 | God Service — รวม OCR + Embed + Rerank + Normalize ไว้ด้วยกัน | 🔴 Critical | Architecture |
|
||
| P4 | Business logic อยู่ใน sidecar แทน backend | 🟡 Medium | Architecture |
|
||
| P5 | VRAM contention logic ล้าสมัย (ออกแบบมาสำหรับ 8GB) | 🟡 Medium | Performance |
|
||
| P6 | `on_event("startup")` deprecated + blocking | 🟡 Medium | Code Quality |
|
||
| P7 | `import tempfile` ซ้ำ | 🟢 Low | Code Quality |
|
||
| P8 | JSON parse fallback ไม่มี warning log | 🟢 Low | Observability |
|
||
|
||
---
|
||
|
||
## VRAM Budget (RTX 5060 Ti 16GB)
|
||
|
||
```
|
||
np-dms-ocr (typhoon-ocr 3B) ~3–4 GB
|
||
np-dms-ai (llama3.2 3B) ~2–3 GB
|
||
BGE-M3 (BAAI/bge-m3) ~2 GB
|
||
Reranker (bge-reranker-large) ~1 GB
|
||
─────────────────────────────────────────
|
||
รวมประมาณ ~8–10 GB ✅ พอดีใน 16GB
|
||
```
|
||
|
||
**ผลกระทบ:** โหลดทุก model พร้อมกันได้ — VRAM Arbiter และ `keep_alive: 0` ไม่จำเป็นอีกต่อไป
|
||
|
||
---
|
||
|
||
## สิ่งที่ควรย้ายไป Backend (NestJS)
|
||
|
||
| สิ่งที่ย้าย | เหตุผล |
|
||
|------------|--------|
|
||
| API Key Authentication | Sidecar อยู่ใน internal Docker network — ไม่ต้องการ auth layer ซ้อน |
|
||
| `systemPrompt` validation + length check | Business rule — backend ควรเป็นผู้กำหนดและ validate ก่อนส่งมา |
|
||
| `/normalize` endpoint ทั้งหมด | Pipeline step ที่ backend orchestrate เอง |
|
||
| Engine selection + alias normalization | Backend ควร resolve engine แล้วส่งชื่อที่ถูกต้องมาตรงๆ |
|
||
| Fast-path text extraction (auto engine) | การตัดสินใจว่า "ต้อง OCR ไหม" เป็น business rule ของ backend |
|
||
| Page range calculation | Backend รู้ document metadata อยู่แล้ว |
|
||
|
||
---
|
||
|
||
## แผนการ Refactor แบ่งเป็น 3 Phase
|
||
|
||
---
|
||
|
||
### Phase 1 — Security & Critical Bugs
|
||
**เป้าหมาย:** แก้ปัญหา critical ที่กระทบ production ทันที
|
||
**ขนาดงาน:** ~1 วัน
|
||
|
||
#### 1.1 ลบ Hardcoded Default API Key
|
||
```python
|
||
# ❌ ก่อน
|
||
OCR_SIDECAR_API_KEY = os.getenv("OCR_SIDECAR_API_KEY", "lcbp3-dms-ocr-sidecar-secure-token-2026")
|
||
|
||
# ✅ หลัง
|
||
OCR_SIDECAR_API_KEY = os.getenv("OCR_SIDECAR_API_KEY")
|
||
if not OCR_SIDECAR_API_KEY:
|
||
raise RuntimeError("OCR_SIDECAR_API_KEY environment variable must be set")
|
||
```
|
||
> ต้อง rotate key ที่ expose ใน git history ด้วย
|
||
|
||
#### 1.2 เปลี่ยน `process_ocr` เป็น Async
|
||
```python
|
||
# ❌ ก่อน
|
||
def process_ocr(...) -> str:
|
||
with httpx.Client(timeout=OCR_TIMEOUT) as client:
|
||
response = client.post(...)
|
||
|
||
# ✅ หลัง
|
||
async def process_ocr(...) -> str:
|
||
async with httpx.AsyncClient(timeout=OCR_TIMEOUT) as client:
|
||
response = await client.post(...)
|
||
```
|
||
|
||
#### 1.3 เปลี่ยน `keep_alive` จาก 0 เป็นค่าที่เหมาะสม
|
||
```python
|
||
# ❌ ก่อน — unload ทันทีเพราะ VRAM ไม่พอ (8GB era)
|
||
"keep_alive": options_override.get("keep_alive", 0)
|
||
|
||
# ✅ หลัง — keep ไว้เพราะ 16GB พอ
|
||
"keep_alive": options_override.get("keep_alive", 300)
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 2 — Performance & Code Quality
|
||
**เป้าหมาย:** ลบ legacy code ที่ออกแบบมาสำหรับ 8GB GPU และปรับปรุง startup
|
||
**ขนาดงาน:** ~1 วัน
|
||
|
||
#### 2.1 ลบ VRAM Contention Logic ทั้งหมด
|
||
```python
|
||
# ❌ ลบออกทั้งหมด
|
||
from services.vram_monitor import get_vram_headroom
|
||
headroom = get_vram_headroom()
|
||
if not headroom.query_success:
|
||
device = "cpu"
|
||
elif headroom.available_mb < threshold_mb:
|
||
device = "cpu"
|
||
```
|
||
|
||
```python
|
||
# ✅ แทนด้วย fixed device
|
||
bge_model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True) # fp16 ได้แล้วบน 16GB
|
||
# device = "cuda" เสมอ — ไม่ต้อง dynamic selection
|
||
```
|
||
|
||
#### 2.2 เปลี่ยน Startup ไปใช้ `lifespan`
|
||
```python
|
||
# ❌ ก่อน — deprecated
|
||
@app.on_event("startup")
|
||
def load_bge_models():
|
||
bge_model = BGEM3FlagModel(...)
|
||
|
||
# ✅ หลัง
|
||
from contextlib import asynccontextmanager
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
await asyncio.to_thread(load_models) # ไม่ block event loop
|
||
yield
|
||
|
||
app = FastAPI(title="OCR Sidecar", version="3.0.0", lifespan=lifespan)
|
||
```
|
||
|
||
#### 2.3 แก้ duplicate import และ JSON parse warning
|
||
```python
|
||
# ลบ import tempfile ที่ซ้ำใน /ocr-upload
|
||
|
||
# เพิ่ม log warning ใน JSON parse fallback
|
||
try:
|
||
result_text = json.loads(raw_text).get("natural_text", raw_text)
|
||
except (json.JSONDecodeError, AttributeError):
|
||
logger.warning(f"[DIAG] Failed to parse JSON response, using raw text. Preview: {raw_text[:100]}")
|
||
result_text = raw_text
|
||
```
|
||
|
||
#### 2.4 Validate `pdf_path` ก่อนส่งเข้า `process_ocr`
|
||
```python
|
||
# เพิ่มใน _process_pdf_doc
|
||
resolved_path = pdf_path or (str(doc.name) if hasattr(doc, 'name') and doc.name else None)
|
||
if not resolved_path or resolved_path in ("", "<memory>"):
|
||
raise ValueError("Invalid PDF path — ต้องส่ง pdf_path ที่ valid เข้ามาด้วย")
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 3 — Architecture Separation
|
||
**เป้าหมาย:** แยก concerns ออกจากกัน ให้ sidecar เป็น pure compute worker
|
||
**ขนาดงาน:** ~2–3 วัน
|
||
|
||
#### 3.1 ย้าย `/normalize` ไป Backend
|
||
|
||
Backend เรียก PyThaiNLP โดยตรง หรือสร้าง microservice แยก:
|
||
```
|
||
n8n → POST /api/rag/normalize (NestJS) → PyThaiNLP → return normalized text
|
||
```
|
||
ลบ `/normalize` endpoint ออกจาก sidecar ทั้งหมด
|
||
|
||
#### 3.2 ย้าย Authentication ออกจาก Sidecar
|
||
|
||
```yaml
|
||
# docker-compose — จำกัด network แทน API key
|
||
services:
|
||
ocr-sidecar:
|
||
networks:
|
||
- internal # ไม่ expose ออก external network
|
||
# ไม่มี ports mapping ออก host
|
||
```
|
||
|
||
Backend (NestJS) เรียก sidecar ผ่าน internal network โดยไม่ต้องส่ง API key
|
||
|
||
#### 3.3 Sidecar รับ Resolved Input เท่านั้น
|
||
|
||
Backend ทำ pre-processing ก่อนแล้วส่งมา:
|
||
|
||
```
|
||
Backend (NestJS)
|
||
├─ ตรวจสอบ PDF มี text layer หรือไม่ (fast-path decision)
|
||
├─ กำหนด engine ที่จะใช้ (ไม่มี "auto" ใน sidecar)
|
||
├─ validate systemPrompt
|
||
├─ คำนวณ page range
|
||
└─► POST /ocr { engine: "np-dms-ocr", pages: [1,2,3], systemPrompt: "..." }
|
||
```
|
||
|
||
Sidecar เหลือหน้าที่เดียว: **รับ input → เรียก model → คืน result**
|
||
|
||
---
|
||
|
||
## Target Architecture หลัง Refactor
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Backend (NestJS) │
|
||
│ │
|
||
│ - Fast-path text extraction decision │
|
||
│ - Engine selection & validation │
|
||
│ - systemPrompt validation │
|
||
│ - Page range calculation │
|
||
│ - Thai text normalization (PyThaiNLP) │
|
||
│ - Auth & rate limiting │
|
||
└────────────────────┬────────────────────────┘
|
||
│ internal Docker network
|
||
▼
|
||
┌─────────────────────────────────────────────┐
|
||
│ OCR Sidecar (compute only) │
|
||
│ │
|
||
│ POST /ocr ← PDF path + page list │
|
||
│ POST /ocr-upload ← multipart file │
|
||
│ POST /embed ← normalized text │
|
||
│ POST /rerank ← query + chunks │
|
||
│ GET /health │
|
||
│ │
|
||
│ Models (always loaded, CUDA): │
|
||
│ - np-dms-ocr via Ollama (keep_alive=300) │
|
||
│ - BGE-M3 fp16 │
|
||
│ - BGE-Reranker-Large fp16 │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Checklist สรุป
|
||
|
||
### Phase 1 (Critical — ทำก่อน)
|
||
- [ ] ลบ hardcoded default API key + rotate key ใน secrets
|
||
- [ ] เปลี่ยน `process_ocr` เป็น async + `httpx.AsyncClient`
|
||
- [ ] เปลี่ยน `keep_alive` default จาก 0 เป็น 300
|
||
|
||
### Phase 2 (Performance)
|
||
- [ ] ลบ VRAM contention logic ทั้งหมด (`get_vram_headroom`, dynamic device)
|
||
- [ ] เปลี่ยน `use_fp16=False` เป็น `use_fp16=True` สำหรับ BGE models
|
||
- [ ] เปลี่ยน `on_event("startup")` เป็น `lifespan` + `asyncio.to_thread`
|
||
- [ ] ลบ duplicate `import tempfile`
|
||
- [ ] เพิ่ม log warning ใน JSON parse fallback
|
||
- [ ] Validate `pdf_path` ก่อนส่งเข้า `process_ocr`
|
||
|
||
### Phase 3 (Architecture)
|
||
- [ ] ย้าย `/normalize` ไป Backend
|
||
- [ ] ย้าย engine selection + alias normalization ไป Backend
|
||
- [ ] ย้าย fast-path decision ไป Backend
|
||
- [ ] จำกัด sidecar network เป็น internal-only แทน API key auth
|
||
- [ ] ลบ `/normalize`, auth middleware ออกจาก sidecar
|
||
|
||
---
|
||
|
||
*เอกสารนี้จัดทำจากการ code review วันที่ 2026-06-20 — ควร update เมื่อ architecture เปลี่ยน*
|