feat(ocr): PP-OCRv5 ONNX Rust 네이티브 OCR 엔진 #206

Merged
altair823 merged 8 commits from feat/rust-native-ocr into main 2026-06-04 09:24:41 +00:00
Owner

요약

이미지 OCR 에 두 번째 백엔드 paddle-onnx 를 추가한다. 기존 ollama-vision
(원격 vision LM, 이미지당 ~50초)을 default 로 유지하고, [image.ocr] engine = "paddle-onnx"
로 PP-OCRv5(검출 DBNet + 인식 CTC) ONNX 모델을 ort(=2.0.0-rc.9) 로 in-process 실행한다 —
Python 런타임/원격 호출 없이 큰 페이지 CPU <4초, 완전 오프라인.

  • 엔진 (crates/kebab-parse-image/src/paddle_onnx.rs): OcrEngine 두 번째 구현
    OnnxPaddleOcr. det/rec ort::Session 1회 로드(Mutex<Session>), 검출 후처리
    (min-area rect = rotating calipers, unclip = polygon offset)는 clipper2/OpenCV 없이
    pure-Rust. CTC greedy decode(blank=0 / dict / space) + per-region 실제 confidence.
  • config (T7): [image.ocr]det_model/rec_model/dict(override) +
    score_thresh/unclip_ratio/max_boxes serde-default + KEBAB_IMAGE_OCR_* env.
    기존 config 무수정 로드(forward-compat).
  • 팩토리 (T8): kebab-app::build_image_ocr_engine/build_pdf_ocr_engine 가 engine
    문자열로 Box<dyn OcrEngine> 생성. 파이프라인 4 site 를 &dyn OcrEngine 로 전환.
  • 서명 cascade (T9): ingest_config_signature 의 image/pdf 브랜치를
    |ocr:1:{engine}:{engine_version} 로 → engine 전환(ollama↔paddle)/모델 변경 시 영향 자산
    자동 재색인. paddle engine_version(blake3 3-asset)은 per-process 1회만 계산(memo).
  • 버전: workspace 0.26.2 → 0.27.0 (minor — 신규 engine 값 + config 키).

설계 문서: docs/superpowers/specs/2026-06-04-rust-native-ocr-spec.md,
docs/superpowers/plans/2026-06-04-rust-native-ocr-plan.md.
deviation 로그: tasks/HOTFIXES.md (2026-06-04 PP-OCRv5 ONNX).

검증

  • clippy: cargo clippy --workspace --all-targets -j 8 -- -D warnings0 경고.
  • 테스트: cargo test -p kebab-config -p kebab-parse-image -p kebab-app -j 8 전부 통과
    (T7 from_config, T9 서명 (a)(b)(c), T10 dict-mismatch/decode-failure 포함).
  • e2e CER 게이트 (실추론, tests/paddle_e2e.rs, synthetic 한/영 fixture): mean gate CER
    0.0049 ≤ 0.05 (clean_paragraph/korean_heavy/numbers_table/tech_terms = 0.0). PoC 0.024
    baseline 보다 우수. 큰 페이지 3.9초 < 5초.
    • T11 에서 발견·수정한 핵심 버그: unclip_rect 가 corner 를 centroid 방사 확장 → wide/short
      텍스트 박스 높이가 안 커져 글자 윗부분 잘림(ㄷ→ㄴ, , 첫 측정 CER 0.26). PaddleOCR
      pyclipper 처럼 edge 별 polygon offset 으로 재작성 → CER 0.005.
  • 스모크 (release 아닌 debug 바이너리 실측): engine = "paddle-onnx" + provider = "none"
    (lexical-only) 로 이미지 2장 ingest(0 errors). clean_paragraph OCR 결과가 ground-truth 와
    글자 단위 일치, per-region confidence 0.99/0.96/0.95(상수 1.0 아님), stored
    parser_version|ocr:1:paddle-onnx:ppocrv5-mobile-kor-1b55f062d055 폴딩 확인.
    kebab search --mode lexical 가 한국어("검색")·영어("embedding") 모두 FTS5 hit.

자동 머지 금지 — 사용자 리뷰 후 머지.

Assisted-by: Claude Code

## 요약 이미지 OCR 에 두 번째 백엔드 `paddle-onnx` 를 추가한다. 기존 `ollama-vision` (원격 vision LM, 이미지당 ~50초)을 default 로 유지하고, `[image.ocr] engine = "paddle-onnx"` 로 PP-OCRv5(검출 DBNet + 인식 CTC) ONNX 모델을 `ort`(=2.0.0-rc.9) 로 **in-process** 실행한다 — Python 런타임/원격 호출 없이 큰 페이지 CPU **<4초**, 완전 오프라인. - **엔진** (`crates/kebab-parse-image/src/paddle_onnx.rs`): `OcrEngine` 두 번째 구현 `OnnxPaddleOcr`. det/rec `ort::Session` 1회 로드(`Mutex<Session>`), 검출 후처리 (min-area rect = rotating calipers, unclip = polygon offset)는 clipper2/OpenCV 없이 pure-Rust. CTC greedy decode(blank=0 / dict / space) + per-region 실제 confidence. - **config** (T7): `[image.ocr]` 에 `det_model`/`rec_model`/`dict`(override) + `score_thresh`/`unclip_ratio`/`max_boxes` serde-default + `KEBAB_IMAGE_OCR_*` env. 기존 config 무수정 로드(forward-compat). - **팩토리** (T8): `kebab-app::build_image_ocr_engine`/`build_pdf_ocr_engine` 가 engine 문자열로 `Box<dyn OcrEngine>` 생성. 파이프라인 4 site 를 `&dyn OcrEngine` 로 전환. - **서명 cascade** (T9): `ingest_config_signature` 의 image/pdf 브랜치를 `|ocr:1:{engine}:{engine_version}` 로 → engine 전환(ollama↔paddle)/모델 변경 시 영향 자산 자동 재색인. paddle engine_version(blake3 3-asset)은 per-process 1회만 계산(memo). - **버전**: workspace `0.26.2 → 0.27.0` (minor — 신규 engine 값 + config 키). 설계 문서: `docs/superpowers/specs/2026-06-04-rust-native-ocr-spec.md`, `docs/superpowers/plans/2026-06-04-rust-native-ocr-plan.md`. deviation 로그: `tasks/HOTFIXES.md` (2026-06-04 PP-OCRv5 ONNX). ## 검증 - **clippy**: `cargo clippy --workspace --all-targets -j 8 -- -D warnings` → **0 경고**. - **테스트**: `cargo test -p kebab-config -p kebab-parse-image -p kebab-app -j 8` 전부 통과 (T7 from_config, T9 서명 (a)(b)(c), T10 dict-mismatch/decode-failure 포함). - **e2e CER 게이트** (실추론, `tests/paddle_e2e.rs`, synthetic 한/영 fixture): mean gate CER **0.0049** ≤ 0.05 (clean_paragraph/korean_heavy/numbers_table/tech_terms = 0.0). PoC 0.024 baseline 보다 우수. 큰 페이지 3.9초 < 5초. - **T11 에서 발견·수정한 핵심 버그**: `unclip_rect` 가 corner 를 centroid 방사 확장 → wide/short 텍스트 박스 높이가 안 커져 글자 윗부분 잘림(ㄷ→ㄴ, `다`→`나`, 첫 측정 CER 0.26). PaddleOCR pyclipper 처럼 edge 별 polygon offset 으로 재작성 → CER 0.005. - **스모크** (release 아닌 debug 바이너리 실측): `engine = "paddle-onnx"` + `provider = "none"` (lexical-only) 로 이미지 2장 ingest(0 errors). `clean_paragraph` OCR 결과가 ground-truth 와 **글자 단위 일치**, per-region confidence 0.99/0.96/0.95(상수 1.0 아님), stored `parser_version` 에 `|ocr:1:paddle-onnx:ppocrv5-mobile-kor-1b55f062d055` 폴딩 확인. `kebab search --mode lexical` 가 한국어("검색")·영어("embedding") 모두 FTS5 hit. 자동 머지 금지 — 사용자 리뷰 후 머지. Assisted-by: Claude Code
altair823 added 7 commits 2026-06-04 08:37:36 +00:00
T0a: onnxruntime 직접 골든 하네스 → CTC blank/dict 매핑 경험 확정(gt CER 0.000).
T0: 모델 번들 dict+NOTICE(.onnx 는 T12 LFS 결정까지 워크트리 보관).
T1: ort(download-binaries)+imageproc 추가, cargo tree ort rc.9 단일 확인.
PP-OCRv5 ONNX OCR engine on the pinned ort rc.9 (no Python, no oar-ocr dep).
Implements the recognize() pipeline end-to-end (compiles + unit-tested):

- T2: OnnxPaddleOcr skeleton, OcrEngine impl, det/rec Session loaded once
  (Mutex-wrapped → Send+Sync), engine_version = blake3(det+rec+dict) cached
  once at construction, dict bounds-check (11945 lines vs 11947 rec classes).
- T2 preproc: det ImageNet mean/std NCHW + limit_side_len 960 → ×32 round
  (golden 192x900→896x192 pinned); rec height-48 keep-aspect, (x-0.5)/0.5.
- T3 det postproc: threshold 0.3 → imageproc contours → min-area rect via
  pure-Rust rotating calipers + convex hull → mean-prob box-score filter →
  pure-Rust unclip(ratio 1.5). No clipper2/OpenCV.
- T4 crop+rectify: corner ordering + bilinear perspective warp to horizontal.
- T5 rec+CTC: greedy decode with the T0a-confirmed mapping
  (idx0=blank, 1..=11945=dict[idx-1], 11946=space), rec-class bounds-check.
- T6 assembly: reading-order OcrText with per-region bbox + real confidence.

Unit tests (4 pass): det_target_dims golden, convex hull, min-area rect,
unclip expansion. Large *.onnx assets stay untracked pending T12 LFS decision.

Remaining: T7 config overrides, T8 factory (4 sites), T9 signature cascade,
T10 error matrix, T11 gates (clippy/e2e CER), T12 docs+bump+PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T7: OcrCfg gains det_model/rec_model/dict overrides + score_thresh/
unclip_ratio/max_boxes (serde default, KEBAB_IMAGE_OCR_* env). OnnxPaddleOcr::new
threads them via ModelPaths::from_config.
T8: build_image_ocr_engine / build_pdf_ocr_engine factories return
Box<dyn OcrEngine>; match on engine string (ollama-vision|paddle-onnx|err).
ImagePipeline.ocr_engine + pdf_ocr_engine signatures switched to &dyn OcrEngine.
OcrEngine gains model() for the progress label.
T9: ingest_config_signature image/pdf branches emit |ocr:1:{engine}:{engine_version}
(memoized blake3 per asset-triple, m3-safe). Unit tests (a)(b)(c) added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause found at T11 e2e: unclip_rect pushed corners radially from the
centroid. For a wide/short text box the diagonal is near-horizontal, so the box
barely grew in height and clipped character tops (ㄷ→ㄴ, 다→나). Rewrote unclip
as a proper per-edge polygon offset along the rect's own (u,v) axes — height and
width each grow by 2*distance, matching PaddleOCR pyclipper.

Result (synthetic-ocr-bench, real inference): mean gate CER 0.2585 → 0.0049
(clean_paragraph/korean_heavy/numbers_table/tech_terms = 0.0), beating the
0.976 PoC baseline. Big page 3.9s < 5s.

T10: dict-length-mismatch construction error + undecodable-bytes recognize error.
T11 e2e: tests/paddle_e2e.rs CER<=0.05 gate (skips cleanly when assets absent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T11: fix 12 clippy lints in paddle_onnx.rs/paddle_e2e.rs (doc overindent,
finish_non_exhaustive, map_or_else, RangeInclusive::contains, cast_lossless,
is_some_and, usize::from). Full-workspace clippy -D warnings = 0.

Smoke (paddle-onnx, real binary): clean_paragraph OCR verbatim-correct, real
per-region confidence (0.99/0.96/0.95), FTS5 lexical hit on Korean(검색)+
English(embedding), parser_version folds |ocr:1:paddle-onnx:<ver>. Big page
<4s inference (5.6s ingest incl. one-time session load).

T12: README [image.ocr].engine + ARCHITECTURE OCR row + SMOKE paddle-onnx config
+ HANDOFF + HOTFIXES dated entry. Workspace version 0.26.2 → 0.27.0 (minor:
new engine value + config keys). .gitattributes: onnx as plain blobs (no git-lfs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
paddle-onnx engine assets — committed as plain binary blobs (git-lfs not
installed on this host; see .gitattributes for the LFS migration recipe).
NOTICE (Apache-2.0) + korean_dict.txt already tracked. Loaded by default from
this dir or KEBAB_IMAGE_OCR_MODEL_DIR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-06-04 08:54:54 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — 핵심 로직(CTC decode / unclip edge-offset / min-area rect / 서명 cascade / 팩토리 4-site / --config facade 스레딩)은 정확하고 회귀 위험 낮음. 머지 가능 수준이나 아래 actionable 보강 후 진행 권장.

[MEDIUM] 골든 회귀 가드 부재: tests/golden/{ctc_rec_golden,det_boxes_clean_paragraph}.json 이 어떤 테스트에서도 소비되지 않음(주석 paddle_onnx.rs:15,550 의 "golden 핀"은 실제 핀 아님). e2e CER 게이트(paddle_e2e.rs)는 /build/dogfood fixture 부재 시 skip 이라 클린 CI 에서 CTC 매핑·unclip 수학의 회귀 가드가 사실상 0. → 골든 JSON 을 deserialize 해 ctc_greedy_decode/unclip_rect/box_score 를 검증하는 CI 상주 단위테스트 추가(모델/ONNX 불요).

[MEDIUM] PDF paddle 튜닝 비대칭: build_pdf_ocr_engine 의 paddle 경로(kebab-app/src/lib.rs:880 부근)가 pdf.ocr.* 대신 image.ocr.* 튜닝(max_pixels/score_thresh/…)을 사용. ollama 경로는 pdf.ocr.* 사용 → 사용자가 [pdf.ocr] 설정 후 paddle 전환 시 무경고로 image 설정 적용(footgun). → 문서화 또는 pdf.ocr 배선.

[MEDIUM] DBNet threshold 명료화: 이진화 threshold 매직넘버 0.3(paddle_onnx.rs:564) 을 명명 const 로 추출 + score_thresh 기본값 0.3(PaddleOCR det_db_box_thresh 기본 0.6 대비 느슨)의 의도 주석.

[LOW] Mutex poison → ingest abort 위험: self.det.lock().expect("…poisoned")(paddle_onnx.rs:346,383) 는 한 자산의 내부 panic 이 전체 ingest 를 abort 시킴(계약은 "자산 skip"). → .unwrap_or_else(|e| e.into_inner()) 로 복구하거나 에러로 매핑.

[LOW] dead field: DetBox.score(#[allow(dead_code)]) 미사용 → region confidence 에 결합하거나 제거.

리더 독립검증: clippy(--workspace --all-targets -D warnings) 0, 전체 테스트 green(e2e CER 평균 0.0049, 게이트 0.05).

회차 1 — 핵심 로직(CTC decode / unclip edge-offset / min-area rect / 서명 cascade / 팩토리 4-site / `--config` facade 스레딩)은 정확하고 회귀 위험 낮음. 머지 가능 수준이나 아래 actionable 보강 후 진행 권장. **[MEDIUM] 골든 회귀 가드 부재**: `tests/golden/{ctc_rec_golden,det_boxes_clean_paragraph}.json` 이 어떤 테스트에서도 소비되지 않음(주석 paddle_onnx.rs:15,550 의 "golden 핀"은 실제 핀 아님). e2e CER 게이트(paddle_e2e.rs)는 `/build/dogfood` fixture 부재 시 skip 이라 **클린 CI 에서 CTC 매핑·unclip 수학의 회귀 가드가 사실상 0**. → 골든 JSON 을 deserialize 해 `ctc_greedy_decode`/`unclip_rect`/`box_score` 를 검증하는 CI 상주 단위테스트 추가(모델/ONNX 불요). **[MEDIUM] PDF paddle 튜닝 비대칭**: `build_pdf_ocr_engine` 의 paddle 경로(kebab-app/src/lib.rs:880 부근)가 `pdf.ocr.*` 대신 `image.ocr.*` 튜닝(max_pixels/score_thresh/…)을 사용. ollama 경로는 `pdf.ocr.*` 사용 → 사용자가 `[pdf.ocr]` 설정 후 paddle 전환 시 무경고로 image 설정 적용(footgun). → 문서화 또는 `pdf.ocr` 배선. **[MEDIUM] DBNet threshold 명료화**: 이진화 threshold 매직넘버 `0.3`(paddle_onnx.rs:564) 을 명명 const 로 추출 + `score_thresh` 기본값 0.3(PaddleOCR `det_db_box_thresh` 기본 0.6 대비 느슨)의 의도 주석. **[LOW] Mutex poison → ingest abort 위험**: `self.det.lock().expect("…poisoned")`(paddle_onnx.rs:346,383) 는 한 자산의 내부 panic 이 전체 ingest 를 abort 시킴(계약은 "자산 skip"). → `.unwrap_or_else(|e| e.into_inner())` 로 복구하거나 에러로 매핑. **[LOW] dead field**: `DetBox.score`(`#[allow(dead_code)]`) 미사용 → region confidence 에 결합하거나 제거. 리더 독립검증: clippy(`--workspace --all-targets -D warnings`) 0, 전체 테스트 green(e2e CER 평균 0.0049, 게이트 0.05).
altair823 added 1 commit 2026-06-04 09:13:36 +00:00
- [MEDIUM] 골든 CI 단위테스트 2건 추가: ctc_greedy_decode_golden (argmax_idx
  one-hot → decoded 문자열 검증), det_box_score_golden (box_score/unclip_rect
  golden corner 검증). 모델/ONNX 불요, CI 상주.
  ctc_greedy_decode를 자유 함수(ctc_greedy_decode_with_dict)로 추출하여 테스트
  가능하게 함.
- [MEDIUM] PDF paddle 튜닝 비대칭 문서화: build_pdf_ocr_engine에 paddle-onnx가
  image.ocr.* 사용(pdf.ocr.* 아님) 이유 명시 + PdfOcrCfg.engine 필드 doc 갱신.
- [MEDIUM] DBNet 이진화 매직넘버 0.3 → DET_BIN_THRESH const 추출 + score_thresh
  기본값 느슨한 이유 1줄 주석.
- [LOW] Mutex poison 복구: det/rec .expect("poisoned") →
  .unwrap_or_else(PoisonError::into_inner). 자산 panic이 ingest abort 안 되도록.
- [LOW] DetBox.score dead field 제거 (box_score 결과는 필터에만 사용, 저장 불요).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-06-04 09:20:51 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 actionable 5건 모두 반영 확인(f3a7222):
① 골든 소비 CI 단위테스트 2건(ctc_greedy_decode_golden/det_box_score_golden, ONNX/모델 불요라 클린 CI 상주) — CTC 매핑·box_score/unclip 회귀 가드 확보.
② PDF paddle 튜닝 비대칭 문서화(build_pdf_ocr_engine + PdfOcrCfg.engine doc).
DET_BIN_THRESH const 추출 + score_thresh 기본값 의도 주석.
④ det/rec Mutex poison → into_inner 복구(자산 panic 이 ingest abort 안 함).
DetBox.score dead field + 미사용 class_to_str 제거.
변경 3파일(+142/-46), 스코프 정합(기능 추가 없음).

리더 독립 재검증: cargo clippy --workspace --all-targets -j 8 -- -D warnings 0, 전체 워크스페이스 테스트 green(신규 골든 테스트 + e2e CER 게이트 평균 0.0049 포함). 회귀·blocker 없음. 머지에 동의합니다.

회차 2 — 회차 1 actionable 5건 모두 반영 확인(`f3a7222`): ① 골든 소비 CI 단위테스트 2건(`ctc_greedy_decode_golden`/`det_box_score_golden`, ONNX/모델 불요라 클린 CI 상주) — CTC 매핑·box_score/unclip 회귀 가드 확보. ② PDF paddle 튜닝 비대칭 문서화(`build_pdf_ocr_engine` + `PdfOcrCfg.engine` doc). ③ `DET_BIN_THRESH` const 추출 + `score_thresh` 기본값 의도 주석. ④ det/rec Mutex poison → `into_inner` 복구(자산 panic 이 ingest abort 안 함). ⑤ `DetBox.score` dead field + 미사용 `class_to_str` 제거. 변경 3파일(+142/-46), 스코프 정합(기능 추가 없음). 리더 독립 재검증: `cargo clippy --workspace --all-targets -j 8 -- -D warnings` 0, 전체 워크스페이스 테스트 green(신규 골든 테스트 + e2e CER 게이트 평균 0.0049 포함). 회귀·blocker 없음. 머지에 동의합니다.
altair823 merged commit 01a17acd3f into main 2026-06-04 09:24:41 +00:00
altair823 deleted branch feat/rust-native-ocr 2026-06-04 09:24:44 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#206