refactor(ai): OCR sidecar canonical naming cleanup — typhoon→np-dms, remove hardcoded keys, asyncio.to_thread, ADR-040/041
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
# 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 เปลี่ยน*
|
||||
@@ -0,0 +1,259 @@
|
||||
# 📋 OCR Sidecar — แผนการ Refactor by QWEN
|
||||
|
||||
**Project:** NAP-DMS (OCR Sidecar Modernization)
|
||||
**Target Hardware:** NVIDIA RTX 5060 Ti 16GB
|
||||
**Date:** 2026-06-20
|
||||
**Owner:** Document Intelligence Engine / Senior Full Stack Developer
|
||||
**Status:** 🟡 Planning Phase
|
||||
**ไฟล์:** `ocr-sidecar-refactor-plan-qwen.md`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 1. Executive Summary
|
||||
|
||||
แผนการ Refactor ครั้งนี้มีเป้าหมายเพื่อเปลี่ยน **OCR Sidecar** จาก "Fat Worker ที่แบกรับ Business Logic และ Hardware Decision" ให้กลายเป็น **"Pure Dumb Worker"** ที่โฟกัสเฉพาะการทำ AI Inference เท่านั้น โดยย้าย Orchestration, Security Gatekeeping, และ VRAM Management กลับไปให้ **NestJS Backend** เป็นผู้ควบคุมผ่านกลไก **Global Mutex + Task Queue**
|
||||
|
||||
### 🎯 Key Objectives
|
||||
1. ✅ **Security:** ปิดช่องโหว่ Path Traversal ใน `/ocr` endpoint
|
||||
2. ✅ **Architecture:** แยก Business Logic (DMS Tags, Noise Filtering, Pagination) ออกจาก Inference Layer
|
||||
3. ✅ **Performance:** ลด Latency 2-5 วินาที/Request โดยยกเลิกการย้าย Model ข้าม RAM↔VRAM
|
||||
4. ✅ **Stability:** ป้องกัน OOM Crash บน RTX 5060 Ti 16GB ด้วย Backend-Controlled Mutex
|
||||
5. ✅ **Scalability:** Sidecar รับ Request แบบ "1 หน้า = 1 Request" เพื่อรองรับ Horizontal Scaling
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 2. Architecture Comparison
|
||||
|
||||
### 🔴 Current Architecture (Anti-Pattern)
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────────────────────────┐
|
||||
│ NestJS API │ ──────► │ Python Sidecar (Fat Worker) │
|
||||
│ │ │ ┌───────────────────────────────┐ │
|
||||
│ (Minimal Logic)│ │ │ ❌ Path Validation │ │
|
||||
│ │ │ │ ❌ DMS Tag Injection │ │
|
||||
│ │ │ │ ❌ Noise Filtering │ │
|
||||
│ │ │ │ ❌ Page Loop Orchestration │ │
|
||||
│ │ │ │ ❌ VRAM Decision (per req) │ │
|
||||
│ │ │ │ ❌ Model .to('cuda'/'cpu') │ │
|
||||
│ │ │ └───────────────────────────────┘ │
|
||||
└─────────────────┘ └─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 🟢 Target Architecture (Best Practice)
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ NestJS Backend (Orchestrator) │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||
│ │ Path Guard │ │ Prompt Builder│ │ VRAM Mutex (Global) │ │
|
||||
│ │ (Canonical) │ │ (DMS Tags) │ │ ──► Sequential GPU Ops │ │
|
||||
│ └─────────────┘ └──────────────┘ └────────────────────────┘ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||
│ │ PDF Splitter│ │ Noise Filter │ │ BullMQ Task Queue │ │
|
||||
│ │ (Per Page) │ │ (Regex) │ │ ──► Concurrency Ctrl │ │
|
||||
│ └─────────────┘ └──────────────┘ └────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ HTTP (1 page = 1 request)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Python Sidecar (Pure Dumb Worker) │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ✅ PDF → Image (PyMuPDF) │ │
|
||||
│ │ ✅ Ollama /v1/chat/completions call │ │
|
||||
│ │ ✅ BGE-M3 Embedding (Fixed on GPU at startup) │ │
|
||||
│ │ ✅ BGE-Reranker (Fixed on GPU at startup) │ │
|
||||
│ │ ✅ Thai NLP Normalize (PyThaiNLP) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 3. VRAM Budget Analysis (RTX 5060 Ti 16GB)
|
||||
|
||||
| Component | Model | VRAM Usage | Status |
|
||||
|-----------|-------|------------|--------|
|
||||
| **BGE-M3 + Reranker** | `BAAI/bge-m3` + `bge-reranker-large` | **~4.5 GB** | 🔒 **Resident** (Load once at startup, stay on GPU) |
|
||||
| **np-dms-ocr** (VLM 3B) | Q4_K_M quantized | **~5.0 GB** | 🔄 **Ephemeral** (Loaded on-demand, `keep_alive=0`) |
|
||||
| **np-dms-ai** (LLM 7B-8B) | Q4_K_M quantized | **~6.0 GB** | 🔄 **Ephemeral** (Loaded on-demand, `keep_alive=10m`) |
|
||||
| **CUDA Context + OS** | System overhead | **~1.5 GB** | 🔒 **Fixed** |
|
||||
| **Total Peak** | — | **~10.5 GB** | ✅ **Safe** (Headroom ~5.5 GB) |
|
||||
|
||||
### ⚠️ Critical Rule
|
||||
**ห้าม** โหลด `np-dms-ocr` และ `np-dms-ai` พร้อมกันเด็ดขาด (5 + 6 = 11 GB + BGE 4.5 GB = 15.5 GB → OOM Risk)
|
||||
**ทางแก้:** NestJS Backend ต้องใช้ **Mutex** บังคับให้ทำงานแบบ Sequential เท่านั้น
|
||||
|
||||
---
|
||||
|
||||
## 📝 4. Task Breakdown
|
||||
|
||||
### 🔴 Phase 1: Security & Critical Fixes (Priority: CRITICAL)
|
||||
**Scope:** `app.py` only — ต้องทำก่อน Deploy
|
||||
|
||||
| # | Task | File | Status |
|
||||
|---|------|------|--------|
|
||||
| 1.1 | แก้ **Path Traversal** ใน `/ocr` ด้วย Path Canonicalization | `app.py` | ⬜ |
|
||||
| 1.2 | แก้ **Mutable Default Argument** (`options_override: dict = {}`) | `app.py` | ⬜ |
|
||||
| 1.3 | ลบ `import tempfile` ที่ซ้ำซ้อนใน `ocr_upload` | `app.py` | ⬜ |
|
||||
| 1.4 | เปลี่ยน `@app.on_event("startup")` → `lifespan` context manager | `app.py` | ⬜ |
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] ส่ง `pdfPath: "../../../../etc/passwd"` ต้องได้ HTTP 403
|
||||
- [ ] Pytest ผ่าน 100% สำหรับ Security Test Suite
|
||||
|
||||
---
|
||||
|
||||
### 🟠 Phase 2: Move Business Logic to Backend (Priority: HIGH)
|
||||
**Scope:** NestJS Backend + `app.py` simplification
|
||||
|
||||
| # | Task | File | Status |
|
||||
|---|------|------|--------|
|
||||
| 2.1 | สร้าง `PromptBuilderService` ใน NestJS (Inject DMS Tags) | `backend/src/ocr/prompt-builder.service.ts` | ⬜ |
|
||||
| 2.2 | สร้าง `PdfSplitterService` (แยก PDF เป็น N หน้า) | `backend/src/ocr/pdf-splitter.service.ts` | ⬜ |
|
||||
| 2.3 | สร้าง `OcrNoiseFilterService` (Regex-based cleanup) | `backend/src/ocr/noise-filter.service.ts` | ⬜ |
|
||||
| 2.4 | สร้าง `OcrOrchestratorService` (Loop + Concurrent Calls) | `backend/src/ocr/orchestrator.service.ts` | ⬜ |
|
||||
| 2.5 | **ลบ** DMS Tag injection ออกจาก `process_ocr()` | `app.py` | ⬜ |
|
||||
| 2.6 | **ลบ** `filter_ocr_noise()` ออกจาก Sidecar | `app.py` | ⬜ |
|
||||
| 2.7 | **ลบ** Page loop ออกจาก `_process_pdf_doc()` (รับ page_num เดียว) | `app.py` | ⬜ |
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Sidecar รับ Request แบบ "1 หน้า = 1 Request" เท่านั้น
|
||||
- [ ] Backend สามารถประกอบ Prompt แบบ Dynamic ได้ (รองรับ Metadata Fields ใหม่ๆ)
|
||||
- [ ] Concurrent OCR 5 หน้าพร้อมกัน ทำได้ผ่าน BullMQ
|
||||
|
||||
---
|
||||
|
||||
### 🟡 Phase 3: VRAM & GPU Management (Priority: HIGH)
|
||||
**Scope:** `app.py` + NestJS Mutex
|
||||
|
||||
| # | Task | File | Status |
|
||||
|---|------|------|--------|
|
||||
| 3.1 | **ลบ** `bge_model.model.to("cuda"/"cpu")` ออกจาก `/embed`, `/rerank` | `app.py` | ⬜ |
|
||||
| 3.2 | **แก้** `load_bge_models()` ให้ `.to("cuda")` ครั้งเดียวตอน Startup | `app.py` | ⬜ |
|
||||
| 3.3 | **ลบ** `get_vram_headroom()` decision logic (เหลือแค่ Log) | `app.py` | ⬜ |
|
||||
| 3.4 | สร้าง `VramMutexService` ใน NestJS (Global Async Lock) | `backend/src/gpu/vram-mutex.service.ts` | ⬜ |
|
||||
| 3.5 | สร้าง `GpuTaskQueue` (BullMQ) สำหรับ OCR/Chat/Rerank | `backend/src/gpu/gpu-queue.service.ts` | ⬜ |
|
||||
| 3.6 | ตั้ง `keep_alive=0` สำหรับ `np-dms-ocr` ใน Ollama config | `docker-compose.yml` | ⬜ |
|
||||
| 3.7 | ตั้ง `keep_alive=10m` สำหรับ `np-dms-ai` ใน Ollama config | `docker-compose.yml` | ⬜ |
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] BGE-M3 โหลดเข้า GPU ครั้งเดียวตอน Container Start
|
||||
- [ ] ไม่เกิด OOM Crash แม้รัน OCR + Chat สลับกัน 100 รอบ
|
||||
- [ ] Latency ของ `/embed` ลดลงจาก ~3s → ~0.3s ต่อ Request
|
||||
|
||||
---
|
||||
|
||||
### 🟢 Phase 4: Sidecar Simplification (Priority: MEDIUM)
|
||||
**Scope:** `app.py` cleanup
|
||||
|
||||
| # | Task | File | Status |
|
||||
|---|------|------|--------|
|
||||
| 4.1 | เปลี่ยน `httpx.Client` → Global `httpx.AsyncClient` (Connection Pool) | `app.py` | ⬜ |
|
||||
| 4.2 | เปลี่ยน endpoint ทั้งหมดเป็น `async def` | `app.py` | ⬜ |
|
||||
| 4.3 | เปลี่ยน `process_ocr()` → `async def process_ocr()` | `app.py` | ⬜ |
|
||||
| 4.4 | เพิ่ม OpenTelemetry tracing (span per request) | `app.py` | ⬜ |
|
||||
| 4.5 | เพิ่ม Prometheus metrics (`ocr_requests_total`, `inference_duration_seconds`) | `app.py` | ⬜ |
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Sidecar รองรับ 50 concurrent requests ได้โดยไม่ Timeout
|
||||
- [ ] มี Grafana Dashboard แสดง Latency p95/p99
|
||||
|
||||
---
|
||||
|
||||
## 📦 5. File Changes Summary
|
||||
|
||||
### 🗑️ Files to DELETE / Simplify
|
||||
| File | Action | Reason |
|
||||
|------|--------|--------|
|
||||
| `app.py::filter_ocr_noise()` | **Delete** | ย้ายไป NestJS |
|
||||
| `app.py::DMS tag injection` | **Delete** | ย้ายไป NestJS PromptBuilder |
|
||||
| `app.py::Page loop` | **Delete** | ย้ายไป NestJS Orchestrator |
|
||||
| `app.py::VRAM decision` | **Delete** | ย้ายไป NestJS Mutex |
|
||||
| `services/vram_monitor.py` | **Delete** | ไม่จำเป็นแล้ว |
|
||||
|
||||
### 🆕 Files to CREATE (NestJS)
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `backend/src/ocr/prompt-builder.service.ts` | ประกอบ Prompt + Inject DMS Tags |
|
||||
| `backend/src/ocr/pdf-splitter.service.ts` | แยก PDF เป็น Buffer ต่อหน้า |
|
||||
| `backend/src/ocr/noise-filter.service.ts` | Regex-based text cleanup |
|
||||
| `backend/src/ocr/orchestrator.service.ts` | จัดการ Page Loop + Concurrency |
|
||||
| `backend/src/gpu/vram-mutex.service.ts` | Global Async Lock สำหรับ GPU Ops |
|
||||
| `backend/src/gpu/gpu-queue.service.ts` | BullMQ Queue สำหรับ GPU Tasks |
|
||||
|
||||
### ✏️ Files to MODIFY
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `app.py` | Simplify to Pure Worker (~50% reduction) |
|
||||
| `docker-compose.yml` | เพิ่ม Ollama `keep_alive` config |
|
||||
| `Modelfile` | Sync options กับ Sidecar payload |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 6. Definition of Done (DoD)
|
||||
|
||||
### 🔒 Security
|
||||
- [ ] Path Traversal test ผ่าน 100%
|
||||
- [ ] API Key validation ครอบคลุมทุก endpoint
|
||||
- [ ] `systemPrompt` length validation ทำงานถูกต้อง
|
||||
|
||||
### ⚡ Performance
|
||||
- [ ] `/embed` latency < 500ms (p95)
|
||||
- [ ] `/rerank` latency < 800ms (p95)
|
||||
- [ ] OCR per page < 30s (รวม cold start)
|
||||
- [ ] Concurrent 5 pages OCR ทำได้ภายใน 60s
|
||||
|
||||
### 🛡️ Stability
|
||||
- [ ] ไม่เกิด OOM Crash ใน 24-hour stress test
|
||||
- [ ] Sidecar auto-recover จาก Ollama timeout ได้
|
||||
- [ ] VRAM usage คงที่ (ไม่เกิด memory leak)
|
||||
|
||||
### 📊 Observability
|
||||
- [ ] Structured logging (JSON) ในทุก endpoint
|
||||
- [ ] Prometheus metrics exposed ที่ `/metrics`
|
||||
- [ ] Grafana dashboard พร้อมใช้งาน
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 7. Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| **OOM Crash** เมื่อโหลด 2 LLM พร้อมกัน | 🔴 Critical | NestJS Mutex บังคับ Sequential + Ollama `keep_alive=0` |
|
||||
| **Path Traversal** ใน `/ocr` | 🔴 Critical | Canonicalization + Base Path Whitelist |
|
||||
| **BGE-M3 Load ช้า** ตอน Startup | 🟡 Medium | Pre-download model ใน Dockerfile (no runtime download) |
|
||||
| **Ollama Cold Start** (~65s) | 🟡 Medium | ใช้ Warm-up endpoint ตอน Container Start |
|
||||
| **VRAM Fragmentation** จาก `.to()` calls | 🟡 Medium | **ลบ** `.to()` calls ออกทั้งหมด (Phase 3) |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 8. Rollout Plan
|
||||
|
||||
```
|
||||
Week 1: Phase 1 (Security) ────────────────► Deploy to Staging
|
||||
Week 2: Phase 3 (VRAM) ────────────────────► Load Test 24h
|
||||
Week 3: Phase 2 (Move Logic to Backend) ───► Integration Test
|
||||
Week 4: Phase 4 (Simplification) ──────────► Production Release
|
||||
```
|
||||
|
||||
### 🔄 Rollback Strategy
|
||||
- ทุก Phase ต้องมี **Feature Flag** เปิด/ปิดได้
|
||||
- Sidecar เก่ายังคง Deploy คู่ขนานได้ 2 สัปดาห์หลัง Release
|
||||
- NestJS Backend สามารถ Fallback ไปใช้ Sidecar เก่าได้ผ่าน Env Var `OCR_SIDECAR_VERSION=v1|v2`
|
||||
|
||||
---
|
||||
|
||||
## 📚 9. References
|
||||
|
||||
- [ADR-023A] OCR Engine Selection (revised 2026-06-11)
|
||||
- [ADR-033] Engine Switching Strategy
|
||||
- [ADR-034] np-dms-ocr as Canonical Engine
|
||||
- [ADR-036] Model Naming Convention
|
||||
- [T015], [T025], [T026-T028] — Technical Specs จาก Change Log
|
||||
- [NAP-DMS Spec 04-00] Infrastructure & OPS
|
||||
|
||||
---
|
||||
|
||||
**Prepared by:** Document Intelligence Engine
|
||||
**Reviewed by:** _Pending_
|
||||
**Approved by:** _Pending_
|
||||
Reference in New Issue
Block a user