aeeff36 의 PoC doc append (engine-comparison.md L134, L141) 가 F7 (`ccitt.pdf`)
의 conversion engine 을 "ImageMagick `convert -compress Group4`" 로 기록했으나,
실제 tests/fixtures/_synth/flate_ccittfax.sh:77-83 은
`gs -sDEVICE=pdfwrite -dMonoImageFilter=/CCITTFaxEncode -dEncodeMonoImages=true`
flag 사용 (ImageMagick `convert` 호출 0회).
fixture binary (`/Filter [ /CCITTFaxDecode ]`, 2060 bytes) 는 invariant 충족 OK
(Step 2 spec compliance + code quality review verified). historical record 의
factual correction only.
review trail:
- impl result: .omc/reviews/2026-05-27-pdf-ocr-step-02-impl-result.md
- spec review: .omc/reviews/2026-05-27-pdf-ocr-step-02-spec-review-result.md
- code review: .omc/reviews/2026-05-27-pdf-ocr-step-02-code-review-result.md (I1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.6 KiB
7.6 KiB
PoC: 한국어 OCR engine comparison (2026-05-27)
Goal
v0.20.0 sub-item 1 (PDF scanned OCR) 의 risk-aware 검증 — 한국어 OCR engine
선택을 결정하기 위한 PoC. single binary 원칙 (kebab CLAUDE.md) + Ollama 통합
정책 (user project_llm_default) + P9 책+PDF use case 정합 위해 vision LLM
경로 채택 결정.
Setup
- Fixture A:
page1— 일반 한국어 + 한자 + 영문 + 숫자 mix (803 char, 8 sections) - Fixture B:
page2-batchim— 받침 intensive (1724 char, 5 sections: 단순 받침 + 겹받침 + 한자 혼용 + 의미 변화 + 외래어) - Rendering: Pillow + Noto Sans CJK KR, 11pt, 300 DPI, A4 → 2480x3507 PNG.
- Comparison: python-Levenshtein. raw / nows / alnum 세 지표.
- LLM access: remote Ollama at 192.168.0.47:11434 (user default LLM host).
Final comparison (PoC step 3, 모든 engine)
Fixture A — page1 (mixed content, 803 char)
| engine | raw | nows | alnum | latency |
|---|---|---|---|---|
| Tesseract (best LSTM + PSM 6 + kor+eng) | 86.80% | 86.83% | 86.96% | ~1-2s |
| EasyOCR (ko + en) | 87.80% | 87.80% | 89.76% | ~10s |
| PaddleOCR (v3.5) | — bug — | — | — | — |
| gemma4:e4b vision (8B) | 79.45% | 78.86% | 77.09% | 36s |
| qwen2.5vl:3b vision (3.8B) | 95.64% | 95.12% | 94.79% | 45.6s |
Fixture B — page2 batchim (받침 intensive, 1724 char)
| engine | raw | nows | alnum | latency |
|---|---|---|---|---|
| Tesseract (best LSTM + PSM 6 + kor+eng) | 75.23% | 72.12% | 66.77% | ~1-2s |
| EasyOCR (ko + en) | 75.12% | 73.23% | 74.06% | ~10s |
| gemma4:e4b vision | 45.77% | 37.41% | 27.01% | 99.1s |
| qwen2.5vl:3b vision | 85.96% | 84.03% | 81.56% | 105.2s |
핵심 발견
- Tesseract 의 PSM 가 main quality driver: default PSM 3 (auto) 가 처참 (20%), PSM 6 (single block) 강제 시 67-87% 까지 회복. spec 에 명시 필수.
- PaddleOCR v3.5 + PaddlePaddle 3.0+ PIR/oneDNN runtime bug: env 변수 우회 불가. paddlepaddle 2.6 downgrade 필요. production 의존성 churn risk.
- gemma4:e4b vision = transcription 불가: paraphrase / hallucination / 단락 누락. 받침 fixture 27% — Tesseract 67% 대비 -40%p.
- gemma4 family text-post-process = 무효 또는 악화: e4b 로 Tesseract OCR 결과 후처리 시 char accuracy 유지 (받침 fixture) 또는 한자/영문 부분 망가짐 (page1). LLM 후처리 path 폐기.
- qwen2.5vl:3b vision = 최고 quality: page1 94.79% / 받침 81.56% alnum. Tesseract 대비 받침에서 +14.79%p. paraphrase 위주가 아닌 transcription.
- qwen2.5vl latency = Tesseract 의 40-50x slower — 800 page 책 indexing ≈ 10 hours.
Single binary 친화도 (CLAUDE.md core principle)
| engine | runtime dep | distribution path | kebab binary 영향 |
|---|---|---|---|
| Tesseract | libtesseract + libleptonica (~10MB C lib) | apt / brew / MSI | leptess Rust binding — native dep |
| EasyOCR | PyTorch CPU (~700MB) + easyocr | Python venv | sidecar IPC architecture |
| pdfium-render | PDFium native shared lib (~10-20MB) | bblanchon/pdfium-binaries github | static link or runtime download |
| qwen2.5vl via Ollama | Ollama (이미 사용) | 이미 user 환경 | HTTP API 호출만 — 0 native dep |
→ qwen2.5vl 가 single binary 원칙 + user Ollama 통합 정책 양쪽 만족.
Recommended architecture (v0.20.0 sub-item 1)
PDF asset
│
▼
[lopdf::Document::load_mem]
│
▼
per-page loop:
│
├── lopdf text extract ──── text >= threshold (e.g. 50 char) ──► text block
│ │
│ │ (text < threshold = scanned)
│ ▼
├── lopdf image stream / page-rasterize (pure Rust) ─► PNG bytes
│ │
│ ▼
└── Ollama POST /api/generate
model = config.pdf.ocr.model (default: qwen2.5vl:3b)
images = [base64(page_png)]
prompt = config.pdf.ocr.prompt_template (transcription-only)
│
▼
OCR text block (+ provenance: which method, latency)
핵심 결정:
- OCR engine: qwen2.5vl:3b (default). config 로 다른 vision model 선택 가능.
- Architecture: text-detect first + vision LLM fallback (사용자 always-on 결정 reverse). 책 PDF 의 일부 (text PDF 페이지) 가 vision 호출 skip → 평균 cost ↓.
- OcrEngine trait: 기존 OllamaVisionOcr (image 용) 와 동일 trait, vision model config 만 다름. PDF + image 동일 path.
- PDF rendering: lopdf (pure Rust, 이미 dep) 의 page 추출 + image stream. pdfium-render 도입 보류 (additional native lib avoid).
- always-on config option:
pdf.ocr.always_on = false(default),true시 text 있는 페이지도 vision 호출 (사용자 P9 의 책 PDF 최대 recall 시).
미해결 risk
- real-world scanned book PDF baseline 미측정 — 합성 fixture 의 95% / 82% 는 ceiling. 실제 책 scan 의 noise / skew / column / 한글 polyphony 에 qwen2.5vl 의 robustness 미검증.
- lopdf 의 page-rasterize capability 미검증 — lopdf 는 PDF parsing 만 제공. scanned PDF (page = embedded image) 의 image stream 추출은 가능하지만, vector PDF (text + image overlay) 의 page rasterize 는 lopdf 만으로 부족할 수 있음. 대안: 처음에는 image stream 추출만 지원, vector PDF rasterize 는 future.
- latency mitigation: 800-page 책 ≈ 10 hours. async indexing background queue (user 가 책 추가 후 즉시 search 불가) + status reporting + cancellation 필요. v0.20 의 wire schema (ingest_progress.v1) 가 OCR latency 별 reporting 갱신 필요.
- qwen2.5vl 의 한자→한글 변환 미세 paraphrase ("大韓民國" → "대한민국"): 검색 use case 에서는 문제 없음. citation 정확성 측면에서는 약점 — provenance 에 "vision-ocr" 명시로 사용자가 인지 가능.
- GPU 가속 미검증: remote (192.168.0.47) 가 GPU 있다면 latency 3-5x 향상 가능. CPU 환경에서는 본 측정 latency 가 그대로.
lopdf probe (2026-05-27, B1 deliverable)
B2 fixture 합성 후 actual /Filter 측정 (shell grep + Python re 기반 probe):
- F1 (
scanned_page1.pdf):/Filter [ /DCTDecode ]— 466897 bytes, JPEG magicffd8ffe0✅ confirmed.- reportlab drawImage + useA85=0 → DCTDecode stream 직접 (ASCII85 래퍼 없음).
- F2 (
scanned_page2.pdf):/Filter [ /DCTDecode ]— 773781 bytes, JPEG magicffd8ffe0✅ confirmed. - F4 (
mojibake.pdf): DejaVu Sans TTF 기반 (Noto CJK TTC PostScript outlines 미지원 → fallback). ToUnicode CMap absent 확인 (strip via byte-level regex, count=0) ✅. PDF header%PDF-1.3. - F6 (
flate_raw.pdf):/Filter /FlateDecode— 872 bytes, DCTDecode 부재 ✅. Pillow RGB→PDF 가 DCTDecode 를 사용하는 것 확인 → 수동 PDF 작성(zlib.compress) 으로 생성. - F7 (
ccitt.pdf):/Filter [ /CCITTFaxDecode ]— 2060 bytes, DCTDecode 부재 ✅. Pillow bilevel('1') + TIFF group4 → ghostscriptpdfwrite -dMonoImageFilter=/CCITTFaxEncode -dEncodeMonoImages=true경로.
Step 3 의 extract_dctdecode_page_image 의 baseline.
주요 관찰:
- reportlab 기본값 (
useA85=1) 은/Filter [ /ASCII85Decode /DCTDecode ]chain →useA85=0으로 순수 DCTDecode 획득. - Pillow
Image.new('RGB').save('.pdf','PDF')는 DCTDecode (JPEG) 로 저장 — FlateDecode raw pixel 이 필요한 F6 는 수동 PDF 작성 필요. - ghostscript pdfwrite default 가 TIFF 입력 시
/undefined in II*로 실패 —-dMonoImageFilter=/CCITTFaxEncode -dEncodeMonoImages=trueflag 로 우회 (ImageMagick fallback 불필요).