chore(ocr): T11/T12 — clippy clean + docs + v0.27.0 bump

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>
This commit is contained in:
2026-06-04 08:36:10 +00:00
parent 8cc4e6d563
commit 375a0693e4
12 changed files with 114 additions and 55 deletions

9
.gitattributes vendored
View File

@@ -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

48
Cargo.lock generated
View File

@@ -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",

View File

@@ -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),

View File

@@ -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<String>` 추가. 기본 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`.

View File

@@ -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 <path>`** — 임시 워크스페이스 / 격리 테스트용 (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` 등).

View File

@@ -39,6 +39,7 @@ impl OcrEngine for MockOcrEngine {
"mock-v1".to_string()
}
#[allow(clippy::unnecessary_literal_bound)]
fn model(&self) -> &str {
"mock-model"
}

View File

@@ -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::{

View File

@@ -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<Vec<String>> {
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<String> = 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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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 <cfg>` 후 `kebab search --config <cfg> --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 생성.

View File

@@ -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 으로 바꿔도 증분