From 478dafeab68bc37ca83b0e86c293370bc3bee79d Mon Sep 17 00:00:00 2001 From: altair823 Date: Fri, 28 Aug 2026 18:31:44 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20PR=20#240=20=ED=9A=8C=EC=B0=A8=203=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81=20=E2=80=94=20=ED=9A=8C?= =?UTF-8?q?=EC=B0=A8=202=20=EA=B0=80=20=EA=B3=A0=EC=B9=98=EB=A9=B4?= =?UTF-8?q?=EC=84=9C=20=EC=83=88=EB=A1=9C=20=EB=A7=8C=EB=93=A0=20=EB=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회차 3 은 3 건으로 수렴했고 그중 둘이 회차 2 가 만든 것이다. 1. 릴리스 경계를 바로잡다가 반대쪽으로 과하게 밀었다. "이 버그가 망칠 수 있었던 스캔본은 사실상 전부 v0.33.0 이 만든 기록" 이라고 썼는데, 페이지 렌더링(#232)이 v0.33.0 신규인 건 맞지만 스캔 PDF OCR 자체는 그보다 오래됐다. `build_pdf_ocr_engine` 의 paddle 분기를 태그별로 세면 v0.28.0=0, v0.30.0=0, v0.31.0=1, v0.32.0=1 이다. v0.31.0 부터 PDF OCR 엔진으로 paddle-onnx 를 고를 수 있었고 그때 `run_rec` 에는 폭 가드가 없었다. 다만 v0.32.0 까지는 단일 DCTDecode 이미지 페이지만 대상이었다. #232 는 래스터 경로를 임의 인코딩까지 넓힌 것이지 만든 것이 아니다. (리뷰는 v0.28.0 이라고 했으나 태그별로 직접 세어 v0.31.0 로 적었다.) 2. 색인 후에 쓸 대안 쿼리의 단위가 틀렸다. 앞 문장은 영향 **문서** 수를 세라는데 `pdf_ocr_events` 는 페이지마다 한 행이고 유니크 제약도 없다 — 40 쪽을 잃은 문서 하나가 40 을 더하고, 수정 전 색인을 두 번 돌렸으면 또 두 배다. `count(DISTINCT doc_id)` 가 둘 다 접는다(이 경로는 doc_id 를 무조건 넘기므로 NULL 로 빠지는 행이 없다). 그리고 `'ocr_error'` 는 `recognize()` 의 모든 실패를 받는 통칭이라 기본 엔진 ollama-vision 을 쓰는 KB 도 #239 와 무관한 행을 쌓는다. V008 에 `ocr_engine` 컬럼이 이미 있으니 조건으로 거른다. paddle-onnx 안에서도 상한이라는 점은 문장으로 밝혔다. 3. ARCHITECTURE.md 에서 이 PR 이 다시 쓴 줄이 하필 이 PR 이 "조용히 무시된다" 고 문서화한 옛 키(`[image.ocr] engine`)를 쓰고 있었다. 같은 표 바로 아래 PDF 행은 이미 `[ingest.pdf.ocr]` 형태다. 곁다리: 회차 3 델타가 mock_ocr.rs 에 rustfmt 어긋남을 1 건 만들었다 (main 0 → 1). 이 PR 은 새 어긋남 0 을 유지해 왔으므로 되돌렸다. 검증: 워크스페이스 1301 passed / 0 failed, clippy -D warnings 무경고, 손댄 파일의 rustfmt 어긋남이 main 과 동일(새로 만든 것 0). Co-Authored-By: Claude Opus 5 (1M context) --- crates/kebab-app/tests/common/mock_ocr.rs | 3 +-- docs/ARCHITECTURE.md | 2 +- tasks/HOTFIXES.md | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/kebab-app/tests/common/mock_ocr.rs b/crates/kebab-app/tests/common/mock_ocr.rs index 4dda829..f687271 100644 --- a/crates/kebab-app/tests/common/mock_ocr.rs +++ b/crates/kebab-app/tests/common/mock_ocr.rs @@ -57,8 +57,7 @@ impl OcrEngine for MockOcrEngine { // plain Display shows only the outer layer. A single-layer error // would render identically under `{}` and `{:#}`, so it could not // tell whether the provenance note keeps the cause (issue #239). - return Err(anyhow::anyhow!("mock inner cause")) - .context("mock failure"); + return Err(anyhow::anyhow!("mock inner cause")).context("mock failure"); } let mut idx = self.call_index.lock().unwrap(); let text = self diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4d83a1a..12c2380 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) | `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) **불변식**: rec 세션 입력 폭은 `REC_MIN_WIDTH = 5` 이상이어야 한다 — 백본이 `T = ceil((w-4)/8)` 개의 CTC 타임스텝을 내므로 `w <= 4` 는 특징맵을 0 열로 접어 ORT 가 세션 전체를 실패시킨다. 그 실패는 `recognize` 의 박스 루프를 뚫고 나가 이미 인식한 박스까지 전부 버리므로, 얇은 크롭은 세션에 넣지 말고 빈 문자열로 돌려보내야 한다 (issue #239). `parser_version = "image-meta-v2"` (issue #239 에서 v1 → v2, 기존 색인 이미지 재처리 유발). | +| 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 선택은 `[ingest.image.ocr] engine`, 팩토리는 `kebab-app::build_image_ocr_engine`. e2e CER 0.005 / 큰 페이지 <4초. (HOTFIXES P6-2, 2026-06-04) **불변식**: rec 세션 입력 폭은 `REC_MIN_WIDTH = 5` 이상이어야 한다 — 백본이 `T = ceil((w-4)/8)` 개의 CTC 타임스텝을 내므로 `w <= 4` 는 특징맵을 0 열로 접어 ORT 가 세션 전체를 실패시킨다. 그 실패는 `recognize` 의 박스 루프를 뚫고 나가 이미 인식한 박스까지 전부 버리므로, 얇은 크롭은 세션에 넣지 말고 빈 문자열로 돌려보내야 한다 (issue #239). `parser_version = "image-meta-v2"` (issue #239 에서 v1 → v2, 기존 색인 이미지 재처리 유발). | | 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 자동 다운로드 | diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index 74daad4..f261100 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -79,9 +79,9 @@ PDF 노트의 `{e:#}` 도 `ocr_engine_failure_surfaces_as_warning` 이 고정한 36 장 중 4 장은 수정 후에도 0 자인데, 글자가 없는 사진(예: `photos/Aphid_2007_1.jpg`, 진딧물 접사)이라 정상이다. 이 이슈로 인한 손실과 "원래 글자가 없어서 비는 문서"를 혼동하면 안 된다 — 성공한 204 장 중에서도 31 장은 det 가 박스를 못 찾아 정상적으로 0 자다. KB 쪽에서 영향 문서를 셀 때는 본문 길이가 아니라 `provenance_json LIKE '%Invalid input shape%'` 로 걸러야 한다. 다만 이 쿼리에는 조건이 둘 붙는다. -**언제 세느냐.** 새 바이너리로 `kebab ingest` 를 돌리기 **전에** 세야 한다. 아래 §재색인 의 `parser_version` bump 가 해당 경로의 documents 행을 지우고 다시 쓰므로, 한 번 색인한 뒤에는 이 쿼리가 0 을 돌려준다. "업그레이드 → 색인 → 릴리스 노트 읽기" 순서로 가면 안 당한 것처럼 보인다. 이미 색인해 버렸다면 PDF 쪽은 `SELECT count(*) FROM pdf_ocr_events WHERE success = 0 AND reason = 'ocr_error'` 로 아직 셀 수 있다 — `pdf_ocr_events` 는 documents 에 FK 가 없어 purge 를 넘겨 살아남는다(`logging.retention_days` 기본 30 일 prune 만 받는다). +**언제 세느냐.** 새 바이너리로 `kebab ingest` 를 돌리기 **전에** 세야 한다. 아래 §재색인 의 `parser_version` bump 가 해당 경로의 documents 행을 지우고 다시 쓰므로, 한 번 색인한 뒤에는 이 쿼리가 0 을 돌려준다. "업그레이드 → 색인 → 릴리스 노트 읽기" 순서로 가면 안 당한 것처럼 보인다. 이미 색인해 버렸다면 PDF 쪽은 `SELECT count(DISTINCT doc_id) FROM pdf_ocr_events WHERE success = 0 AND reason = 'ocr_error' AND ocr_engine = 'paddle-onnx'` 로 아직 셀 수 있다 — `pdf_ocr_events` 는 documents 에 FK 가 없어 purge 를 넘겨 살아남는다(`logging.retention_days` 기본 30 일 prune 만 받는다). `count(*)` 가 아니라 `count(DISTINCT doc_id)` 인 이유는 이 표가 **페이지마다** 한 행이고 유니크 제약도 없어서, 40 쪽을 잃은 문서 하나가 40 을 더하고 수정 전 색인을 두 번 돌렸으면 또 두 배가 되기 때문이다. `ocr_engine` 을 거는 이유는 `'ocr_error'` 가 `recognize()` 의 모든 실패를 받는 통칭이라, 기본 엔진인 ollama-vision 을 쓰는 KB 에서도 #239 와 무관한 행이 쌓이기 때문이다. 이렇게 걸러도 paddle-onnx 의 다른 실패까지 포함하는 상한이라는 점은 남는다. -**스캔 PDF 는 이 필터로 안 걸린다.** 이번 수정 이전에 나간 **모든** 릴리스에서 — v0.33.0 을 포함해서 — PDF 경로는 노트를 `err={}` 로 찍었고, anyhow 의 기본 Display 는 가장 바깥 context 하나(`rec session run`)만 내보낸다. 하필 스캔 PDF 페이지 렌더링(#232) 자체가 v0.33.0 에서 처음 나갔으므로, 이 버그가 망칠 수 있었던 스캔본은 사실상 전부 그 한 릴리스가 만든 기록이다. 그래서 스캔본까지 세려면 `OR provenance_json LIKE '%err=rec session run%'` 을 반드시 함께 걸어야 한다. 이미지 경로는 처음부터 `{err:#}` 라 체인 전체가 들어갔고, 이번에 PDF 쪽도 `{e:#}` 로 맞췄으므로 앞으로 색인되는 것은 양쪽 다 첫 필터에 걸린다. +**스캔 PDF 는 이 필터로 안 걸린다.** 이번 수정 이전에 나간 **모든** 릴리스에서 — v0.33.0 을 포함해서 — PDF 경로는 노트를 `err={}` 로 찍었고, anyhow 의 기본 Display 는 가장 바깥 context 하나(`rec session run`)만 내보낸다. 노출 구간은 이렇다 — PDF OCR 엔진으로 paddle-onnx 를 고를 수 있게 된 것이 v0.31.0 이므로 그때부터 스캔 PDF 도 같은 손실을 겪을 수 있었고, 다만 v0.32.0 까지는 단일 DCTDecode 이미지 페이지만 OCR 대상이었다. 페이지 렌더링(#232)이 임의 인코딩까지 넓힌 v0.33.0 에서 대상이 크게 늘었으니 기록의 대부분은 그 릴리스가 만든 것이겠지만, 전부는 아니다. 그래서 스캔본까지 세려면 `OR provenance_json LIKE '%err=rec session run%'` 을 반드시 함께 걸어야 한다. 이미지 경로는 처음부터 `{err:#}` 라 체인 전체가 들어갔고, 이번에 PDF 쪽도 `{e:#}` 로 맞췄으므로 앞으로 색인되는 것은 양쪽 다 첫 필터에 걸린다. ### 재색인: 버전 두 개를 올렸다