From 375a0693e47e42e7d1318e912b18db2c69735804 Mon Sep 17 00:00:00 2001 From: altair823 Date: Thu, 4 Jun 2026 08:36:10 +0000 Subject: [PATCH] =?UTF-8?q?chore(ocr):=20T11/T12=20=E2=80=94=20clippy=20cl?= =?UTF-8?q?ean=20+=20docs=20+=20v0.27.0=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:. 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 --- .gitattributes | 9 ++-- Cargo.lock | 48 ++++++++++---------- Cargo.toml | 2 +- HANDOFF.md | 1 + README.md | 3 +- crates/kebab-app/tests/common/mock_ocr.rs | 1 + crates/kebab-parse-image/src/lib.rs | 2 +- crates/kebab-parse-image/src/paddle_onnx.rs | 37 ++++++++------- crates/kebab-parse-image/tests/paddle_e2e.rs | 11 ++--- docs/ARCHITECTURE.md | 4 +- docs/SMOKE.md | 18 ++++++++ tasks/HOTFIXES.md | 33 ++++++++++++++ 12 files changed, 114 insertions(+), 55 deletions(-) diff --git a/.gitattributes b/.gitattributes index 3d8921e..39a96ba 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,6 @@ -# PP-OCRv5 ONNX OCR models (paddle-onnx engine) — large binary, store via Git LFS -# to keep clone / `cargo package` lean. Enable with `git lfs install` before commit. -*.onnx filter=lfs diff=lfs merge=lfs -text +# PP-OCRv5 ONNX OCR models (paddle-onnx engine). git-lfs is not installed on +# this host, so they are committed as plain binary blobs (treated as binary — +# no textual diff/merge). If/when git-lfs becomes available, migrate with +# `git lfs migrate import --include='*.onnx'` and restore the filter line: +# *.onnx filter=lfs diff=lfs merge=lfs -text +*.onnx -text diff --git a/Cargo.lock b/Cargo.lock index af34b62..0e97d51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4751,7 +4751,7 @@ dependencies = [ [[package]] name = "kebab-app" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -4799,7 +4799,7 @@ dependencies = [ [[package]] name = "kebab-chunk" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "blake3", @@ -4817,7 +4817,7 @@ dependencies = [ [[package]] name = "kebab-cli" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "clap", @@ -4838,7 +4838,7 @@ dependencies = [ [[package]] name = "kebab-config" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "dirs 5.0.1", @@ -4854,7 +4854,7 @@ dependencies = [ [[package]] name = "kebab-core" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "blake3", @@ -4868,7 +4868,7 @@ dependencies = [ [[package]] name = "kebab-embed" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "blake3", @@ -4882,7 +4882,7 @@ dependencies = [ [[package]] name = "kebab-embed-candle" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "candle-core", @@ -4902,7 +4902,7 @@ dependencies = [ [[package]] name = "kebab-embed-local" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "fastembed", @@ -4915,7 +4915,7 @@ dependencies = [ [[package]] name = "kebab-embed-ollama" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "kebab-config", @@ -4930,7 +4930,7 @@ dependencies = [ [[package]] name = "kebab-eval" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "kebab-app", @@ -4949,7 +4949,7 @@ dependencies = [ [[package]] name = "kebab-llm" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "kebab-core", @@ -4958,7 +4958,7 @@ dependencies = [ [[package]] name = "kebab-llm-local" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "kebab-config", @@ -4975,7 +4975,7 @@ dependencies = [ [[package]] name = "kebab-mcp" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "kebab-app", @@ -4993,7 +4993,7 @@ dependencies = [ [[package]] name = "kebab-nli" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "hf-hub", @@ -5008,7 +5008,7 @@ dependencies = [ [[package]] name = "kebab-parse-code" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "gix", @@ -5031,7 +5031,7 @@ dependencies = [ [[package]] name = "kebab-parse-image" -version = "0.26.2" +version = "0.27.0" dependencies = [ "ab_glyph", "anyhow", @@ -5059,7 +5059,7 @@ dependencies = [ [[package]] name = "kebab-parse-md" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "kebab-core", @@ -5076,7 +5076,7 @@ dependencies = [ [[package]] name = "kebab-parse-pdf" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "blake3", @@ -5091,7 +5091,7 @@ dependencies = [ [[package]] name = "kebab-rag" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "blake3", @@ -5113,7 +5113,7 @@ dependencies = [ [[package]] name = "kebab-search" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "globset", @@ -5132,7 +5132,7 @@ dependencies = [ [[package]] name = "kebab-source-fs" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "blake3", @@ -5150,7 +5150,7 @@ dependencies = [ [[package]] name = "kebab-store-sqlite" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "blake3", @@ -5170,7 +5170,7 @@ dependencies = [ [[package]] name = "kebab-store-vector" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "arrow", @@ -5194,7 +5194,7 @@ dependencies = [ [[package]] name = "kebab-tui" -version = "0.26.2" +version = "0.27.0" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 249b8fa..cdd068a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ edition = "2024" rust-version = "1.85" license = "MIT OR Apache-2.0" repository = "https://github.com/altair823/kebab" -version = "0.26.2" # v0.26.2 — ingest 설정 변경 시 영향 자산 자동 재색인: ingest 산출에 영향 주는 설정(청킹/이미지 OCR·caption/pdf.ocr/[ingest.code])의 결정적 서명을 effective parser_version(skip 비교 + 저장 doc 필드 양쪽)에 폴딩 → 해당 설정 변경 시 `--force-reingest` 없이 영향 자산만 자동 재색인. 비산출 설정(search/rag/ui/log + max_pixels/languages/timeout 등)은 제외(과도 무효화 회피). doc_id 는 base parser_version 으로 안정 유지(orphan churn 회피). 결과 포맷·CLI·wire 불변(내부 skip 판정 정정) → patch. — CLAUDE.md §Release +version = "0.27.0" # v0.27.0 — PP-OCRv5 ONNX Rust 네이티브 OCR 엔진: `[image.ocr] engine = "paddle-onnx"` (default 여전히 "ollama-vision") 로 in-process 검출+인식(`ort` =2.0.0-rc.9, Python 런타임 0). DBNet det + CTC rec, 후처리(min-area rect/unclip)는 pure-Rust. e2e CER 0.005(synthetic 한/영, PoC 0.024 대비 우수), 큰 페이지 CPU <4초(Ollama vision ~50초 대비). 신규 config `det_model`/`rec_model`/`dict`/`score_thresh`/`unclip_ratio`/`max_boxes` + `KEBAB_IMAGE_OCR_*` env. ingest 서명 `|ocr:1:{engine}:{engine_version}` 로 engine/모델 변경 시 자동 재색인. 신규 인터페이스(engine 값/config 키) → minor. — CLAUDE.md §Release # pre-v0.18 workspace-wide cleanup: enable clippy::pedantic group with # intentional allow-list. The allowed lints are either cosmetic (doc style), diff --git a/HANDOFF.md b/HANDOFF.md index 478464a..92a7638 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -35,6 +35,7 @@ P0~P5 직렬. P6~P9 P5 이후 병렬 가능. 머지 후 발견된 모든 deviation / hotfix 의 dated 로그는 [tasks/HOTFIXES.md](tasks/HOTFIXES.md). 본 요약은 \"누군가가 인수받을 때 알아두면 시간을 많이 절약하는\" 항목만: +- **2026-06-04 PP-OCRv5 ONNX Rust 네이티브 OCR** — v0.27.0. `[image.ocr] engine = "paddle-onnx"` 로 PP-OCRv5(검출+인식) ONNX 를 in-process(`ort` =2.0.0-rc.9) 실행 — Python 런타임/원격 호출 없이 큰 페이지 CPU <4초(Ollama vision ~50초 대비). default 는 여전히 `"ollama-vision"`. 후처리(min-area rect/unclip)는 pure-Rust. **함정**: unclip 은 corner 를 centroid 에서 방사 확장하면 안 되고 edge 별 polygon offset 이어야 함(방사 확장 시 wide/short 텍스트 박스 높이가 안 커져 글자 윗부분 잘림 → ㄷ→ㄴ, e2e CER 0.26). 수정 후 CER 0.005. 모델 ONNX 는 `crates/kebab-parse-image/assets/paddleocr-onnx/`(LFS). 자세한 내용: `tasks/HOTFIXES.md` (2026-06-04 PP-OCRv5 ONNX), spec/plan `docs/superpowers/{specs,plans}/2026-06-04-rust-native-ocr-*.md`. - **2026-06-03 ingest 설정 변경 자동 재색인** — v0.26.2. ingest 산출에 영향 주는 설정(청킹/이미지 OCR·caption/pdf.ocr/`[ingest.code]`)을 변경하면 `--force-reingest` 없이 영향 자산만 자동 재색인. 그 설정들의 결정적 서명(`ingest_config_signature`)을 effective parser_version(skip 비교 + 저장 doc 필드 양쪽)에 폴딩 → 다음 ingest 비교가 mismatch. 비산출 설정(search/rag/ui/log + max_pixels/languages/timeout)은 제외(과도 무효화 회피), doc_id 는 base 로 안정 유지. **업그레이드 후 첫 ingest 는 전 자산 1회 재색인**(저장된 상수 parser_version ≠ 새 composite; embedding 은 V012 캐시 히트). 결과 포맷·CLI·wire 불변(내부 skip 판정 정정). 자세한 내용: `tasks/HOTFIXES.md` (2026-06-03 ingest 설정 변경 자동 재색인), spec/plan `docs/superpowers/{specs,plans}/2026-06-03-*invalidation*.md`. - **2026-06-03 ingest 진행 로그 개선** — v0.26.1. 이미지/PDF + OCR/caption on 볼트 ingest 가 "멈춘 듯" 보이던 문제 해소: TTY 진행바에 현재 파일명 + 느린 phase(ocr/caption/embed)+모델명 + 경과초 `(Ns)` heartbeat, 종료 시 최장 소요 파일 top-5 요약. 신규 wire `asset_phase{idx,total,phase,model}` + `asset_timings.ocr_ms`/`caption_ms`(additive, `ingest_progress.v1` 유지, serde default 0). 이미지·PDF 경로도 `asset_timings` emit(이전 markdown 만). 기본 동작 불변. 자세한 내용: `tasks/HOTFIXES.md` (2026-06-03 ingest 진행 로그), spec/plan `docs/superpowers/{specs,plans}/2026-06-03-ingest-log-improve-*.md`. - **2026-06-03 arctic-embed-l-v2.0 임베더 통합** — v0.26.0. 별칭 제거 후 설명형 query recall 보강(측정 recall@10 130/132, e5 +7). `kebab-embed-candle` 모델 레지스트리화(e5 mean + `snowflake-arctic-embed-l-v2.0` CLS, 모델별 pooling/prefix) + 신규 `kebab-embed-ollama`(`provider="ollama"`, `/api/embed`). config `endpoint: Option` 추가. 기본 e5 유지(opt-in), arctic 전환은 embedding_version cascade → 재색인. candle↔Ollama cosine>0.99 게이트로 pooling/prefix 정확성 고정(`#[ignore]`). 자세한 내용: `tasks/HOTFIXES.md` (2026-06-03 arctic), spec `docs/superpowers/specs/2026-06-03-arctic-embedder-spec.md`. diff --git a/README.md b/README.md index 1fad986..5b18783 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,8 @@ nli_threshold = 0.0 # >0 (예: 0.5) 면 mDeBERTa XNLI groundedn - **파생물 캐시** — embedding 결과를 내용 해시로 자동 캐싱한다 (위 「핵심 기능」 참고). 설정 항목 없음. - **`[ingest.code]`** — code ingest 의 skip 정책 (`skip_generated_header`, `max_file_bytes`, `extra_skip_globs`). `.gitignore` 자동 honor, `.kebabignore` 는 추가 layer. -- **`[pdf.ocr]`** — scanned PDF 의 page-단위 OCR (default off / opt-in, page 당 ~수십 초 cost). 활성화 후 v0.19 시절 색인분은 `kebab ingest --force-reingest` 로 재처리. +- **`[image.ocr]`** — 이미지 OCR (default off / opt-in). `engine` 으로 백엔드 선택: `"ollama-vision"` (default, 원격 vision LM) 또는 `"paddle-onnx"` (v0.27.0 신규 — PP-OCRv5 ONNX 를 in-process 로 실행, Python 런타임 불필요, 큰 페이지 CPU <4초, 오프라인). `paddle-onnx` 는 워크스페이스에 번들된 모델을 쓰며 `det_model`/`rec_model`/`dict` 로 경로 override, `score_thresh`(0.3)/`unclip_ratio`(1.5)/`max_boxes`(1000) 로 검출 튜닝 가능 (`KEBAB_IMAGE_OCR_*` env 동일 지원). engine 또는 모델을 바꾸면 영향 이미지가 자동 재색인된다. +- **`[pdf.ocr]`** — scanned PDF 의 page-단위 OCR (default off / opt-in, page 당 ~수십 초 cost). `engine` 은 `[image.ocr]` 과 동일하게 `"ollama-vision"`/`"paddle-onnx"` 선택. 활성화 후 v0.19 시절 색인분은 `kebab ingest --force-reingest` 로 재처리. - **`--config `** — 임시 워크스페이스 / 격리 테스트용 (CLI · TUI 모두 honor). - **`kebab config migrate`** — 새 버전에서 추가된 config 섹션을 기존 `config.toml` 에 설명 주석과 함께 채워 넣는다 (사용자가 손본 값·주석·순서는 보존, 멱등, 변경 시 자동 `.bak` 백업). `--dry-run` 으로 변경 미리보기. `kebab doctor` 가 갱신 필요 시 안내한다. `kebab init` 으로 새로 생성되는 config.toml 도 섹션별 주석을 포함한다. - **`KEBAB_*` env** — 일부 키 override (`KEBAB_RAG_SCORE_GATE`, `KEBAB_EVAL_GOLDEN` 등). diff --git a/crates/kebab-app/tests/common/mock_ocr.rs b/crates/kebab-app/tests/common/mock_ocr.rs index 144e88d..efc3732 100644 --- a/crates/kebab-app/tests/common/mock_ocr.rs +++ b/crates/kebab-app/tests/common/mock_ocr.rs @@ -39,6 +39,7 @@ impl OcrEngine for MockOcrEngine { "mock-v1".to_string() } + #[allow(clippy::unnecessary_literal_bound)] fn model(&self) -> &str { "mock-model" } diff --git a/crates/kebab-parse-image/src/lib.rs b/crates/kebab-parse-image/src/lib.rs index ab93677..177724e 100644 --- a/crates/kebab-parse-image/src/lib.rs +++ b/crates/kebab-parse-image/src/lib.rs @@ -34,7 +34,7 @@ pub mod paddle_onnx; pub use caption::{apply_caption, caption_image}; pub use ocr::{OLLAMA_VISION_ENGINE, OcrEngine, OllamaVisionOcr, apply_ocr}; -pub use paddle_onnx::{OnnxPaddleOcr, PADDLE_ONNX_ENGINE, engine_version_for_config}; +pub use paddle_onnx::{ModelPaths, OnnxPaddleOcr, PADDLE_ONNX_ENGINE, engine_version_for_config}; use anyhow::{Context, Result}; use kebab_core::{ diff --git a/crates/kebab-parse-image/src/paddle_onnx.rs b/crates/kebab-parse-image/src/paddle_onnx.rs index e519866..b469326 100644 --- a/crates/kebab-parse-image/src/paddle_onnx.rs +++ b/crates/kebab-parse-image/src/paddle_onnx.rs @@ -3,13 +3,13 @@ //! production dependency (see crate-level rationale + `assets/paddleocr-onnx/NOTICE`). //! //! Pipeline (`recognize`): -//! 1. decode (RGB) + downscale long edge to `max_pixels` -//! 2. det: ImageNet-normalized NCHW → DBNet prob map `[1,1,H,W]` -//! → threshold 0.3 → contours → min-area rect (rotating calipers, -//! pure Rust) → unclip(ratio 1.5, pure Rust) → boxes -//! 3. crop+rectify: perspective warp each rotated box to a horizontal strip -//! 4. rec: 48×W normalized `(x-0.5)/0.5` → `[1,T,11947]` → CTC greedy decode -//! 5. assemble reading-order `OcrText` +//! 1. decode (RGB) + downscale long edge to `max_pixels` +//! 2. det: ImageNet-normalized NCHW → DBNet prob map `[1,1,H,W]` → threshold +//! 0.3 → contours → min-area rect (rotating calipers, pure Rust) → +//! unclip(ratio 1.5, pure Rust) → boxes +//! 3. crop+rectify: perspective warp each rotated box to a horizontal strip +//! 4. rec: 48×W normalized `(x-0.5)/0.5` → `[1,T,11947]` → CTC greedy decode +//! 5. assemble reading-order `OcrText` //! //! ## Confirmed CTC facts (empirically derived in T0a, see //! `tests/golden/ctc_rec_golden.json` — do NOT re-derive): @@ -82,7 +82,7 @@ impl std::fmt::Debug for OnnxPaddleOcr { .field("unclip_ratio", &self.unclip_ratio) .field("max_boxes", &self.max_boxes) .field("max_pixels", &self.max_pixels) - .finish() + .finish_non_exhaustive() } } @@ -100,11 +100,10 @@ impl ModelPaths { /// Default bundled-asset directory: `KEBAB_IMAGE_OCR_MODEL_DIR` if set, /// else the crate's `assets/paddleocr-onnx/`. pub fn from_default_dir() -> Self { - let dir = std::env::var("KEBAB_IMAGE_OCR_MODEL_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| { - Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/paddleocr-onnx") - }); + let dir = std::env::var("KEBAB_IMAGE_OCR_MODEL_DIR").map_or_else( + |_| Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/paddleocr-onnx"), + PathBuf::from, + ); Self { det: dir.join("ppocrv5_mobile_det.onnx"), rec: dir.join("korean_ppocrv5_mobile_rec.onnx"), @@ -211,7 +210,7 @@ impl OnnxPaddleOcr { match idx { CTC_BLANK => None, CTC_SPACE => Some(" "), - i if i >= 1 && i <= DICT_LINES => Some(self.dict[i - 1].as_str()), + i if (1..=DICT_LINES).contains(&i) => Some(self.dict[i - 1].as_str()), _ => None, // out-of-range guard (should not happen for 11947 classes) } } @@ -226,6 +225,10 @@ impl OcrEngine for OnnxPaddleOcr { self.engine_version.clone() } + // The trait method's elided lifetime ties the return to `&self`; the body + // returns a literal, but the signature must match the trait, so allow the + // `'static`-narrowing lint here. + #[allow(clippy::unnecessary_literal_bound)] fn model(&self) -> &str { // Static label for the progress display; the per-asset hash lives // in `engine_version`. @@ -335,7 +338,7 @@ impl OnnxPaddleOcr { for (x, y, px) in det_img.enumerate_pixels() { let (xi, yi) = (x as usize, y as usize); for c in 0..3 { - let v = px[c] as f32 / 255.0; + let v = f32::from(px[c]) / 255.0; arr[[0, c, yi, xi]] = (v - IMAGENET_MEAN[c]) / IMAGENET_STD[c]; } } @@ -372,7 +375,7 @@ impl OnnxPaddleOcr { for (x, y, px) in resized.enumerate_pixels() { let (xi, yi) = (x as usize, y as usize); for c in 0..3 { - let v = px[c] as f32 / 255.0; + let v = f32::from(px[c]) / 255.0; arr[[0, c, yi, xi]] = (v - 0.5) / 0.5; // [-1, 1] } } @@ -447,7 +450,7 @@ fn load_dict(path: &Path) -> Result> { let raw = std::fs::read_to_string(path)?; // split on '\n'; drop a single trailing empty element from the final newline let mut lines: Vec = raw.split('\n').map(|s| s.trim_end_matches('\r').to_string()).collect(); - if lines.last().map(|s| s.is_empty()).unwrap_or(false) { + if lines.last().is_some_and(String::is_empty) { lines.pop(); } Ok(lines) diff --git a/crates/kebab-parse-image/tests/paddle_e2e.rs b/crates/kebab-parse-image/tests/paddle_e2e.rs index 36f8e08..1b56d2a 100644 --- a/crates/kebab-parse-image/tests/paddle_e2e.rs +++ b/crates/kebab-parse-image/tests/paddle_e2e.rs @@ -33,7 +33,7 @@ fn cer(gt: &str, pred: &str) -> f64 { for i in 1..=m { let mut cur = vec![i; n + 1]; for j in 1..=n { - let cost = if g[i - 1] == p[j - 1] { 0 } else { 1 }; + let cost = usize::from(g[i - 1] != p[j - 1]); cur[j] = (prev[j] + 1).min(cur[j - 1] + 1).min(prev[j - 1] + cost); } prev = cur; @@ -42,11 +42,10 @@ fn cer(gt: &str, pred: &str) -> f64 { } fn fixture_dir() -> PathBuf { - std::env::var("KEBAB_TEST_OCR_FIXTURE_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| { - PathBuf::from("/build/dogfood/corpus/images/synthetic-ocr-bench") - }) + std::env::var("KEBAB_TEST_OCR_FIXTURE_DIR").map_or_else( + |_| PathBuf::from("/build/dogfood/corpus/images/synthetic-ocr-bench"), + PathBuf::from, + ) } /// T10: undecodable image bytes must surface as an error (the kebab-app caller diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7b5b70e..f586925 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -20,7 +20,7 @@ Cargo workspace, 함수 호출 기반 모듈러 모놀리스. UI binary (`kebab- | 한국어 형태소분석 | `lindera-ko-dic` (FTS5 외부 tokenizer, v0.20.1) — 2자 이상 한국어 query 지원 | | LLM | Ollama HTTP (default `gemma4:e4b` ─ OCR / caption 와 family 통일. 사용자가 더 큰 variant `gemma4:26b` 등으로 override 가능) | | 음성 ASR | `whisper.cpp` (via `whisper-rs`) — P8 보류, 시스템 dep brainstorm 후 | -| OCR (image) | Ollama vision LM (default `gemma4:e4b`) — `OcrEngine` trait 으로 Tesseract / Apple Vision 등 future swap (HOTFIXES P6-2) | +| OCR (image) | `OcrEngine` trait, 2 백엔드: **`ollama-vision`** (default, `gemma4:e4b`) / **`paddle-onnx`** (v0.27.0 — PP-OCRv5 ONNX in-process via `ort` =2.0.0-rc.9, DBNet det + CTC rec, 후처리 min-area rect/unclip pure-Rust, Python 런타임 0). engine 선택은 `[image.ocr] engine`, 팩토리는 `kebab-app::build_image_ocr_engine`. e2e CER 0.005 / 큰 페이지 <4초. (HOTFIXES P6-2, 2026-06-04) | | OCR (PDF, v0.20.0+) | Ollama vision LM (default `qwen2.5vl:3b`) — post-extract enrichment via `kebab-app::pdf_ocr_apply` (H-1 resolution). DCTDecode-only v1 (FlateDecode/CCITTFax skip + warning). family asymmetry vs image OCR: PoC alnum 94.79% (qwen2.5vl) >> 27% (gemma4:e4b 받침), 본 단계에서 PDF OCR 만 qwen2.5vl. | | Image caption | Ollama vision LM, runtime gate `image.caption.enabled` (default OFF) | | RAG groundedness 검증 | `kebab-nli` 의 mDeBERTa-v3 XNLI 가 `(packed_chunks, generated_answer)` entailment 검사 (fb-41). `[rag] nli_threshold > 0` (default 0 = disabled, production 권장 0.5) 일 때 활성 — 미달 시 `refusal_reason = nli_verification_failed` (LLM self-judge ceiling 보완). 첫 호출 시 ~280 MB ONNX 자동 다운로드 | @@ -212,7 +212,7 @@ kebab/ │ ├── kebab-rag/ # RAG pipeline (P4-3) │ ├── kebab-nli/ # NLI verifier (mDeBERTa-v3 XNLI, fb-41 PR-9a/9b/9c-1) │ ├── kebab-eval/ # golden query runner + metrics (P5-1, P5-2) -│ ├── kebab-parse-image/ # ImageExtractor + Ollama OCR + caption (P6) +│ ├── kebab-parse-image/ # ImageExtractor + OCR (ollama-vision + paddle-onnx ONNX) + caption (P6) │ ├── kebab-parse-pdf/ # lopdf per-page text extractor (P7-1) │ ├── kebab-parse-code/ # tree-sitter AST extractors: Rust (P10-1A-2), Python + TypeScript + JavaScript (P10-1B), Go (P10-1C-Go), Java + Kotlin (P10-1C-JK — java.rs + kotlin.rs), C + C++ (P10-1D — c.rs + cpp.rs); chunker lives in kebab-chunk │ ├── kebab-app/ # facade (P0 시그니처 + P3-5/P6-4/P7-3 본체). src/derivation_payload.rs = 캐시 payload 인코딩 (v0.21.0) diff --git a/docs/SMOKE.md b/docs/SMOKE.md index 941e105..2dbbb67 100644 --- a/docs/SMOKE.md +++ b/docs/SMOKE.md @@ -358,6 +358,24 @@ lang_hint = "kor" 이미지 자산 한 장당 OCR 1 호출 + Caption 1 호출 → ~3-6초 (`gemma4:e4b` 기준). 다이어그램 / 카메라 사진 / 스크린샷 위주 워크스페이스에 권장. 책 / 스캔본은 P7 PDF 라인으로. +**v0.27.0 — paddle-onnx 엔진 (오프라인, Ollama 불필요).** `[image.ocr] engine = "paddle-onnx"` 로 바꾸면 PP-OCRv5 ONNX 를 in-process 로 실행한다 (원격 vision LM 불필요, 큰 페이지 CPU <4초). embedding 까지 끄려면 `[models.embedding] provider = "none"` (lexical-only) 로 두면 Ollama 없이 OCR→FTS5 검색 전체 경로를 스모크할 수 있다: + +```toml +[models.embedding] +provider = "none" # lexical-only — Ollama 불필요 + +[image.ocr] +enabled = true +engine = "paddle-onnx" # PP-OCRv5 ONNX in-process (Python/원격 0) +model = "ppocrv5-mobile-kor" +languages = ["kor", "eng"] +max_pixels = 1600 +# det_model / rec_model / dict 로 번들 모델 경로 override 가능 (생략 시 번들 사용) +# score_thresh = 0.3 / unclip_ratio = 1.5 / max_boxes = 1000 으로 검출 튜닝 +``` + +스모크: `kebab ingest --config ` 후 `kebab search --config --mode lexical "<이미지 안 한국어 단어>"` 가 그 image chunk 를 반환하면 OCR→FTS5 wiring 정상. engine 또는 모델을 바꾸면 다음 ingest 가 영향 이미지를 자동 재색인한다. + ## P7-3 PDF ingestion `config.toml` 의 `[workspace] include` 에 `**/*.pdf` 를 추가하면 `kebab ingest` 가 텍스트 PDF 자산도 색인합니다. 외부 service 의존 없음 — `kebab-parse-pdf` 가 lopdf 로 페이지 단위 텍스트 추출, `kebab-chunk::PdfPageV1Chunker` 가 페이지 경계를 절대 넘지 않는 chunk 생성. diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index e52583d..4c2b678 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -14,6 +14,39 @@ historical contract that was implemented; this file accumulates the deltas so phase 5+ readers can find the live behavior without diffing git history. +## 2026-06-04 — PP-OCRv5 ONNX Rust 네이티브 OCR 엔진 (v0.27.0) + +**무엇을 추가했나.** 이미지 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초. `OcrEngine` trait 의 두 번째 구현 +`OnnxPaddleOcr`(`crates/kebab-parse-image/src/paddle_onnx.rs`), 팩토리는 +`kebab-app::build_image_ocr_engine`/`build_pdf_ocr_engine` (`match engine`). 검출 후처리 +(min-area rect = rotating calipers, unclip = polygon offset)는 clipper2/OpenCV 없이 pure-Rust. + +**T11 e2e 에서 발견·수정한 핵심 버그 (unclip).** 첫 실측 CER 이 0.26(게이트 0.05) 으로 크게 +초과. 단계 골든(`crates/kebab-parse-image/tests/golden/`) 와 prediction dump 로 국소화한 결과 +`unclip_rect` 가 corner 를 centroid 기준 **방사(radial) 확장**하고 있었다. 텍스트 박스는 +wide/short(예 586×15)라 대각선이 거의 수평 → 방사 확장 시 corner 가 수평으로만 ~11px 움직이고 +**세로로는 거의 안 커져** 글자 윗/아랫부분이 잘렸다(ㄷ→ㄴ 로 `다`→`나`, ascender 손실). +PaddleOCR pyclipper 처럼 **edge 별로 바깥으로 offset**(width·height 각각 2·distance 증가) 하도록 +rect 자체 (u,v) 축 기준 확장으로 재작성. 결과: mean gate CER **0.2585 → 0.0049** +(clean_paragraph/korean_heavy/numbers_table/tech_terms = 0.0), PoC 0.024 baseline 보다 우수. +큰 페이지 3.9초 < 5초 게이트. **교훈**: 회전 사각형 unclip 은 방사 확장이 아니라 polygon edge +offset 이어야 한다. + +**Config / 서명 cascade.** `[image.ocr]` 에 `det_model`/`rec_model`/`dict`(Option, override) + +`score_thresh`(0.3)/`unclip_ratio`(1.5)/`max_boxes`(1000) serde-default 필드 + `KEBAB_IMAGE_OCR_*` +env 추가(기존 config 무수정 로드 — forward-compat). `ingest_config_signature` 의 image/pdf 브랜치를 +`|ocr:1:{model}` → `|ocr:1:{engine}:{engine_version}` 로 바꿔 engine 전환(ollama↔paddle) 또는 +모델 변경 시 영향 자산 자동 재색인. paddle engine_version 은 모델 3-asset blake3 를 **per-process +1회만** 계산(triple 키 memo) — 자산마다 17MB 재해시 회피. + +**모델 배포.** ONNX 2개(det 4.7MB / rec 13MB) + dict + NOTICE 를 `crates/kebab-parse-image/ +assets/paddleocr-onnx/` 에 둔다(Git LFS). 테스트는 `KEBAB_IMAGE_OCR_MODEL_DIR`(기본 = 번들 dir) +에서 로드, e2e(`tests/paddle_e2e.rs`)는 모델/fixture 부재 시 깨끗이 skip(CI green). 자세한 설계: +spec/plan `docs/superpowers/{specs,plans}/2026-06-04-rust-native-ocr-*.md`. + ## 2026-06-03 — ingest 출력 영향 설정 변경 시 영향 자산 자동 재색인 (v0.26.2) **무엇이 깨졌나.** `[image.ocr]` / `[image.caption]` 를 off→색인→on 으로 바꿔도 증분