diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1b2f4..d56fde3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ | 버전 | 요약 | 노트 | |---|---|---| +| **v0.33.1** | #239 얇은 검출 박스 하나가 이미지 OCR 전체를 날리던 문제 — paddle-onnx rec 입력 폭 하한(5) 추가. 실측 실패 36/240 → 0/240, 되살아난 본문 11,747 자. parser_version cascade 로 기존 색인 이미지·PDF 자동 재처리. | [release](https://gitea.altair823.xyz/altair823-org/kebab/releases/tag/v0.33.1) · [notes](docs/release-notes/v0.33.1-draft.md) | | **v0.33.0** | 28.4k 문서 도그푸딩이 드러낸 다섯 결함(#228–#232) — 스캔 PDF 페이지 렌더링(CCITT/JBIG2/Flate/JPX 커버), chunks_fts 삭제 800배, Lance 압축, sweep 진행 표시, 임베딩 캐시 계측. | [release](https://gitea.altair823.xyz/altair823-org/kebab/releases/tag/v0.33.0) · [notes](docs/release-notes/v0.33.0-draft.md) | | **v0.32.0** | ponytail-audit over-engineering 정리 arc — 죽은 search-cache scaffold 제거, 9 AST chunker→1 통합(byte-identical), shim crate 흡수(crate 22→20), FusionPolicy/YAML/NLI. | [release](https://gitea.altair823.xyz/altair823-org/kebab/releases/tag/v0.32.0) · [notes](docs/release-notes/v0.32.0-draft.md) | | **v0.31.0** | 척추 단순화 + 캐시 전면화 — tui·multi-turn 세션·candle 제거, config v4→v5, image/pdf/code 임베딩 캐시, OCR/caption derivation 캐시. | [release](https://gitea.altair823.xyz/altair823-org/kebab/releases/tag/v0.31.0) · [notes](docs/release-notes/v0.31.0-draft.md) | diff --git a/Cargo.lock b/Cargo.lock index 192c64b..41f5613 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4256,7 +4256,7 @@ dependencies = [ [[package]] name = "kebab-app" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -4300,7 +4300,7 @@ dependencies = [ [[package]] name = "kebab-chunk" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "blake3", @@ -4318,7 +4318,7 @@ dependencies = [ [[package]] name = "kebab-cli" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "clap", @@ -4338,7 +4338,7 @@ dependencies = [ [[package]] name = "kebab-config" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "dirs 5.0.1", @@ -4354,7 +4354,7 @@ dependencies = [ [[package]] name = "kebab-core" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "blake3", @@ -4369,7 +4369,7 @@ dependencies = [ [[package]] name = "kebab-embed-local" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "fastembed", @@ -4382,7 +4382,7 @@ dependencies = [ [[package]] name = "kebab-embed-ollama" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "kebab-config", @@ -4397,7 +4397,7 @@ dependencies = [ [[package]] name = "kebab-eval" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "kebab-app", @@ -4416,7 +4416,7 @@ dependencies = [ [[package]] name = "kebab-llm-local" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "kebab-config", @@ -4432,7 +4432,7 @@ dependencies = [ [[package]] name = "kebab-mcp" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "kebab-app", @@ -4450,7 +4450,7 @@ dependencies = [ [[package]] name = "kebab-nli" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "hf-hub", @@ -4465,7 +4465,7 @@ dependencies = [ [[package]] name = "kebab-parse-code" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "gix", @@ -4488,7 +4488,7 @@ dependencies = [ [[package]] name = "kebab-parse-image" -version = "0.33.0" +version = "0.33.1" dependencies = [ "ab_glyph", "anyhow", @@ -4515,7 +4515,7 @@ dependencies = [ [[package]] name = "kebab-parse-md" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "kebab-core", @@ -4532,7 +4532,7 @@ dependencies = [ [[package]] name = "kebab-parse-pdf" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "blake3", @@ -4549,7 +4549,7 @@ dependencies = [ [[package]] name = "kebab-rag" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "blake3", @@ -4570,7 +4570,7 @@ dependencies = [ [[package]] name = "kebab-search" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "globset", @@ -4588,7 +4588,7 @@ dependencies = [ [[package]] name = "kebab-source-fs" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "blake3", @@ -4606,7 +4606,7 @@ dependencies = [ [[package]] name = "kebab-store-sqlite" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "blake3", @@ -4626,7 +4626,7 @@ dependencies = [ [[package]] name = "kebab-store-vector" -version = "0.33.0" +version = "0.33.1" dependencies = [ "anyhow", "arrow", diff --git a/Cargo.toml b/Cargo.toml index 649bee2..c9c5b0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ edition = "2024" rust-version = "1.85" license = "MIT OR Apache-2.0" repository = "https://github.com/altair823/kebab" -version = "0.33.0" # v0.33.0 — 28.4k 문서 도그푸딩이 드러낸 다섯 결함(#228–#232). minor 트리거는 #232 의 parser_version cascade(pdf-text-v1 → v2, 기존 색인 PDF 재처리 유발) + 신규 config 키([ingest.pdf.ocr] render_library / render_dpi). (1) 스캔 PDF 를 페이지 렌더링으로 OCR(#232): CCITTFax/JBIG2/Flate/JPX 가 조용히 버려지던 것을 pdfium 런타임 로드로 커버, 없으면 DCTDecode 폴백 + 이유 명시(ocr_skipped_pages / failure_reason / doctor pdf_render). 단일 바이너리 유지(ldd 에 pdfium 없음). (2) chunks_fts 삭제를 rowid 조회로(#229, V016): 문서 200건 삭제 1590초 → 2.0초, 검색 결과 불변. (3) Lance fragment 압축 + 삭제 배치화(#230): 2.8만 문서 색인 속도 분당 32~36건 평탄 유지(이전 30.7 → 4.3 단조 하락), 15 GB → 2.8 GB. (4) 삭제 sweep 을 진행바·ndjson 에 노출(#228): 무표시 구간이 hang 으로 오인되던 것 + sweep 취소 협조. (5) derivation cache 배칭·계측(#231): 가설은 재현 안 됨(히트 139초 vs 우회 1180초), asset_timings 에 cache_hit/miss/ms 노출. 도그푸딩: HOTFIXES 2026-08-16/17(CER 12.6~15.7% 한국어 CCITT 스캔, 이전 본문 0자). 상세 docs/release-notes/v0.33.0-draft.md — CLAUDE.md §Release +version = "0.33.1" # v0.33.1 — #239 얇은 검출 박스 하나가 이미지 OCR 전체를 날리던 문제. paddle-onnx rec 세션은 입력 폭이 4 이하면 특징맵이 0 열로 접혀 실패하는데(타임스텝 = ceil((w-4)/8)), 그 오류가 recognize 의 ? 를 타고 나가 같은 이미지에서 이미 인식해 둔 박스까지 전부 버렸다. REC_MIN_WIDTH = 5 미만은 세션에 넣지 않고 빈 문자열로 돌려보낸다. 색인은 성공으로 끝나므로 검색이 0 건 나올 때까지 안 드러나던 조용한 손실. 실측(도그푸딩 이미지 240 개 파일, 같은 모델·설정): 실패 36/240 → 0/240, 되살아난 본문 11,747 자, 원래 성공하던 204 장은 글자 수 변화 0. 곁다리로 PDF OCR 실패 노트를 {e:#} 로 바꿔 잘려 나가던 ORT 원인을 보존. patch 로 판정한 근거 — 사용자 표면(서브커맨드/플래그/config 키/wire)이 하나도 안 바뀌었다. parser_version cascade(image-meta-v1 → v2, pdf-text-v2 → v3)가 있어 minor 로 볼 여지가 있으나, v0.33.0 의 minor 트리거는 cascade 단독이 아니라 신규 config 키와 짝이었다. 다만 재색인 비용은 minor 급으로 드니(doc_id 전면 교체 + store 재작성) release notes 의 upgrade 절차 참조. 도그푸딩: HOTFIXES 2026-08-28. 상세 docs/release-notes/v0.33.1-draft.md — 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/docs/release-notes/v0.33.1-draft.md b/docs/release-notes/v0.33.1-draft.md new file mode 100644 index 0000000..ba27c87 --- /dev/null +++ b/docs/release-notes/v0.33.1-draft.md @@ -0,0 +1,104 @@ +# v0.33.1 — 얇은 조각 하나가 이미지 OCR 을 통째로 날리던 문제 (#239) + +이미지가 본문에 파일명만 남은 채 색인되던 결함을 고친 릴리스. 이슈 #239 를 등록할 때 실사용 지식 베이스(문서 11,867 / 청크 65,764)에서 잰 값으로는 이미지 문서의 **11.8%**(4,699 장 중 555 장)가 이 상태였다. 그 지식 베이스는 이 저장소에서 재현할 수 없으므로, 아래 실측은 전부 도그푸딩 말뭉치 240 개 파일 기준(**15.0%**)이다. 사용자 표면(명령·플래그·config 키·wire 스키마)에는 변화가 없다. + +## `patch` 로 판정한 근거 + +솔직히 적는다. **CLAUDE.md §Release 의 문면대로는 minor 다.** minor 조건에 "사용자가 받는 결과·동작의 변화" 가 있고 이 수정은 실패하던 문서의 OCR 결과를 0 자에서 실제 본문으로 바꾼다. patch 조건은 "기능·인터페이스 변경이 **없을** 때 … 즉 결과가 같고 새 명령/플래그/config 도 없으면 patch" 라는 두 조건의 AND 인데, 이 릴리스는 뒤 조건만 만족한다. + +그럼에도 patch 로 컷했다. 이유는 사용자가 무엇을 다시 배워야 하느냐다 — 새로 익힐 서브커맨드도, 플래그도, config 키도, 깨지는 wire 소비자도 없고 "원래 되어야 했는데 안 되던 것이 된다" 뿐이다. 직전 v0.33.0 이 minor 였던 것과도 구분된다. 그 릴리스의 트리거는 `parser_version` cascade **단독**이 아니라 신규 config 키(`render_library` / `render_dpi`)와 짝이었다. + +**다만 재색인 비용은 minor 급으로 든다.** 버전 숫자만 보고 가볍게 넘기지 말고 아래 upgrade 절차를 읽을 것. + +실측 evidence 는 `tasks/HOTFIXES.md` 의 2026-08-28 dated entry 에 있다. + +--- + +## 얇은 검출 박스 하나가 그 이미지의 인식 결과를 전부 버렸다 (#239) + +**변경 사실.** paddle-onnx OCR 은 검출한 글자 영역을 하나씩 인식 네트워크에 넣는다. 그런데 아주 얇은 영역 — 표 선 조각, 글자 사이 틈, 사진의 가는 무늬 — 이 하나라도 섞이면 ONNX 런타임이 **세션 실행 전체를 실패**시켰고, 그 오류가 위로 전파되면서 **같은 이미지에서 그때까지 인식해 둔 결과까지 전부 버려졌다.** + +원인은 폭 하한이 없다는 것이었다. 인식 네트워크는 입력 폭을 반복해서 줄이므로 **폭 4 이하** 입력은 도중에 폭 0 인 텐서가 되고, 거기서 Conv 가 터진다. 번들된 모델을 폭 1 부터 48 까지 직접 훑어 보면 경계가 딱 떨어진다. + +| 인식 입력 폭 (높이 48 기준) | 결과 | +|---|---| +| 1 ~ 4 | 전부 실패 — `Invalid input shape: {1,0}` | +| 5 ~ 48 | 전부 성공 | + +이유도 유도된다. 인식 출력의 타임스텝 수가 `ceil((폭 - 4) / 8)` 이라 폭 4 이하에서 0 이 되고, 오류 메시지의 `{1,0}` 이 바로 그 0 이다. 그래서 폭 5 미만은 네트워크에 넣지 않고 그 박스만 버린다. 바로 옆 줄에 이미 "박스 하나가 비면 나머지는 살린다" 는 처리가 있었는데 오류 경로에만 그 방어가 없었다. + +**가장 나쁜 점은 조용했다는 것이다.** 색인은 "성공" 으로 끝난다. 이미지 문서는 본문에 파일명만 남고 스캔 PDF 페이지는 청크가 0 이 되는데, 그 문서를 검색해서 0 건이 나올 때까지 아무 신호가 없다. + +**trade-off.** 사실상 없다. 높이 48 기준 폭 4 이하는 1:12 보다 납작한 조각이라 글자가 들어갈 수 없고, 원래 잘 읽히던 이미지의 결과는 **한 글자도 바뀌지 않는다**(아래 실측 참조). + +하한 값은 이슈가 제안한 16 대신 **5** 를 썼다. 여기서 오해하기 쉬운데, **16 을 써도 실제로 잃는 텍스트는 없다** — 말뭉치 전체에서 세션에 들어간 박스 10,991 개 중 폭 5~16 짜리는 403 개였고 그중 글자를 뱉은 것은 **0 개**다(빈 문자열을 내는 첫 구간이고, 실제로 글자가 나오기 시작하는 건 폭 17 부터다). 5 를 고른 이유는 손실이 아니라 정직함이다. 5 가 그래프의 실제 하한이므로 상수 이름(`REC_MIN_WIDTH`)과 그 주석이 거짓이 되지 않고, 지금 동작하는 범위를 임의로 좁히지도 않는다. + +**mitigation.** 같은 손실이 다시 들어오지 못하도록 양쪽을 막았다. 하한 아래 폭이 네트워크에 닿으면 테스트가 실패하고, 하한을 **16 보다 크게** 올리면 컴파일이 안 된다(`const _: () = assert!(REC_MIN_WIDTH <= 16, …)`). 16 은 위에서 잰 "여기까지는 어차피 아무것도 안 나온다" 의 상한이므로, 그 안에서 값을 조정하는 것은 자유이고 그 위로 넘기려면 새로 측정해야 한다는 뜻이다. + +PDF 쪽 OCR 실패 기록이 원인을 잘라먹던 것도 함께 고쳤다. 예전에는 `err=rec session run` 에서 끝나 실제 원인이 사라졌고, 그 때문에 "어느 문서가 당했나" 를 찾는 질의가 스캔 PDF 를 한 건도 못 찾았다. + +**실측.** 도그푸딩 말뭉치의 이미지 240 개 파일을 같은 모델·같은 설정으로 수정 전후 비교했다. + +| | 수정 전 | 수정 후 | +|---|---|---| +| OCR 성공 | 204 / 240 | **240 / 240** | +| OCR 실패 | 36 (15.0%) | **0** | + +실패는 종류를 가리지 않았다 — 도표 8 · 영문 10 · 한국어 9 · 사진 9. 글자가 없는 사진도 얇은 박스는 검출되므로 똑같이 당했다. 되살아난 본문은 합계 **11,747 자**(중앙값 37 자, 최대 3,950 자)다. + +한 장이 얼마나 손해 보고 있었는지는 `charts/Clickpath_Analysis.png` 이 잘 보여 준다. 이 이미지는 박스를 **119 개** 검출하는데, 수정 전에는 그중 3 개를 인식한 시점에 얇은 조각을 만나 세션이 죽고 그 3 개까지 함께 버려져 본문이 **0 자**였다. 수정 후에는 87 개 영역 **1,116 자**가 나온다. + +부작용이 없다는 것도 확인했다. 원래 성공하던 204 장의 인식 글자 수가 **한 장도 변하지 않았다.** + +--- + +## upgrade 절차 + +**0. 색인하기 전에, 당한 문서를 먼저 세라.** 이 릴리스의 버전 bump 가 문서 행을 다시 쓰므로 한 번 색인하고 나면 아래 질의가 영구히 0 을 돌려준다. 순서를 지키지 않으면 "내 지식 베이스는 안 당했다" 는 정반대 결론에 닿는다. + +```sh +sqlite3 ~/.local/share/kebab/kebab.sqlite # --config 를 쓴다면 그 config 의 storage.data_dir +``` +```sql +SELECT count(*) FROM documents +WHERE provenance_json LIKE '%Invalid input shape%' + OR provenance_json LIKE '%err=rec session run%'; +``` + +두 번째 조건이 필요한 이유는, 이번 수정 이전 릴리스에서 PDF 경로가 오류 원인을 잘라 기록했기 때문이다. + +이미 색인해 버렸다면 PDF 쪽은 아래로 **일부** 셀 수 있다. 다만 두 가지 한계가 있다 — `pdf_ocr_events` 는 매 ingest 시작 시 `logging.retention_days`(기본 30 일)로 정리되므로 오래된 기록은 이미 없고, `reason = 'ocr_error'` 는 모든 OCR 실패를 받는 통칭이라 이 수치는 #239 피해의 **상한**이다. 0 이 나와도 "안 당했다" 는 뜻이 아니다. + +```sql +SELECT count(DISTINCT doc_id) FROM pdf_ocr_events +WHERE success = 0 AND reason = 'ocr_error' AND ocr_engine = 'paddle-onnx'; +``` + +**1. 재색인은 자동이다.** `parser_version` 이 `image-meta-v1` → `image-meta-v2`, `pdf-text-v2` → `pdf-text-v3` 로 올라가서 다음 `kebab ingest` 가 기존 이미지·PDF 를 다시 처리한다. `--force-reingest` 를 떠올릴 필요가 없다. + +단, **workspace 워크가 닿는 문서만** 해당한다. `kebab ingest <파일>` 이나 stdin 으로 넣은 문서는 `/_external/` 로 복사되고 그때 `_external/` 이 `.kebabignore` 에 자동 추가되므로, 이후 workspace 색인에서 방문되지 않는다. 그 경로로 넣은 이미지·PDF 는 원본을 다시 `kebab ingest <파일>` 해야 고쳐진다. + +**2. 재-OCR 범위는 지식 베이스마다 다르다.** OCR 산출물은 소스 바이트를 키로 캐싱되고 실패한 OCR 은 캐시에 저장되지 않았으므로, **캐시 항목이 있는 문서는** 다시 OCR 하지 않고 넘어간다. 그런데 OCR 파생 캐시는 v0.31.0 에서 들어왔고 이미지 `parser_version` 은 v0.28.0 이후 줄곧 `image-meta-v1` 이었다. 그래서 v0.31.0 이전에 이미지를 색인하고 그 뒤로 OCR 설정·모델을 바꾼 적이 없는 지식 베이스는 이미지 쪽 캐시가 비어 있고, 이번 bump 로 **성공했던 이미지까지 전부 다시 OCR 된다.** 이미지가 많다면 소요 시간을 넉넉히 잡을 것. PDF 는 v0.33.0 의 `pdf-text-v2` bump 때 캐시가 채워졌으므로 사정이 낫다. + +**3. 나머지 비용은 무조건 든다.** 문서 식별자(`doc_id`)가 파서 버전에서 파생되므로 이 bump 는 **모든 이미지·PDF 문서의 doc_id 를 바꾼다.** 기존 행이 지워지고 벡터도 지워진 뒤 재파싱·재청킹·재임베딩·재삽입이 이어진다. 임베딩은 캐시에 히트하지만 행은 다시 쓴다. + +`doc_id` 는 `--json` 출력의 필수 필드이자 `kebab inspect doc ` 의 핸들이다. 이전 출력에서 갈무리해 둔 doc_id, 저장한 인용, `integrations/claude-code` 스킬처럼 doc_id 를 들고 있는 소비자는 이번 색인 이후 전부 헛번호가 된다. 파일은 하나도 안 고쳤는데 식별자가 한꺼번에 바뀐다. + +**4. 이미지 OCR 을 안 쓰는 경우에도 재색인은 일어난다.** 기본 OCR 엔진은 `ollama-vision` 이고 이미지 OCR 은 기본 off 라, paddle-onnx 를 쓰지 않는 지식 베이스는 얻는 것 없이 이미지·PDF 를 다시 색인하게 된다. 엔진 범위로 무효화를 좁히는 대안도 있었으나, 기존 캐스케이드 규칙 옆에 두 번째 무효화 경로를 만드는 값이 더 크다고 보고 택하지 않았다. + +--- + +## 어디까지 영향이 있었나 + +paddle-onnx 를 이미지 OCR 엔진으로 고를 수 있게 된 **v0.28.0 이후 모든 릴리스**가 이 결함을 갖고 있었다. 스캔 PDF 도 같은 인식 경로를 타므로 마찬가지인데, v0.32.0 까지는 단일 DCTDecode 이미지 페이지만 OCR 대상이었고 v0.33.0 의 페이지 렌더링(#232)이 대상을 임의 인코딩까지 넓혔으므로 스캔본 피해의 대부분은 v0.33.0 으로 색인한 기록일 가능성이 높다. + +**환경에 따라 오류 메시지가 다르게 보인다.** 이슈에는 실패 노드가 `Conv.33` 으로 적혀 있는데 다른 컴퓨터에서는 `p2o.pd_op.batch_norm_.1.0_nchwc` 로 나온다. ONNX 런타임이 CPU 명령어 집합에 맞춰 그래프를 최적화하면서 노드 이름을 다시 붙이기 때문이고, 상태 메시지와 `{1,0}` 은 같다. 다른 문제가 아니다. + +--- + +## 검증 + +- 워크스페이스 전체 테스트 **1301 passed / 0 failed / 64 ignored** +- `cargo clippy --workspace --all-targets -- -D warnings` 경고 없음 +- 도그푸딩 말뭉치 이미지 240 개 파일 수정 전후 비교 (위 표) +- PR #240 은 5 회차 리뷰를 거쳤고 회차별 검증 통과 지적이 19 → 16 → 3 → 1 → 0 으로 수렴했다 diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index d9320ca..f6e64a3 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -18,7 +18,7 @@ git history. ### 무엇이 문제였나 -`OnnxPaddleOcr::run_rec` 은 검출된 박스를 높이 48 로 리사이즈하면서 **폭을 1 이상으로만** 보장했다. 그런데 PP-OCRv5 rec 백본은 폭을 반복해서 줄이기 때문에 폭이 한 자릿수인 입력은 도중에 폭 0 인 특징맵이 되고, ORT 가 Conv 에서 세션 실행 전체를 실패시킨다. +`OnnxPaddleOcr::run_rec` 은 검출된 박스를 높이 48 로 리사이즈하면서 **폭을 1 이상으로만** 보장했다. 그런데 PP-OCRv5 rec 백본은 폭을 반복해서 줄이기 때문에 **폭 4 이하** 입력은 도중에 폭 0 인 특징맵이 되고, ORT 가 Conv 에서 세션 실행 전체를 실패시킨다. (이슈 본문과 이 항목의 초안이 "한 자릿수" 라고 적었으나 아래 표대로 폭 5~9 는 정상 동작한다.) 그 실패가 `recognize` 의 `?` 를 타고 나가면서, **이미 인식해 둔 나머지 박스가 전부 함께 버려졌다.** 바로 아래 줄(`if text.is_empty() { continue; }`)이 "박스 하나가 비면 나머지는 살린다"는 의도를 담고 있는데 오류 경로에만 그 방어가 없었던 것이다. 손실이 얇은 조각 하나에서 그치지 않고 그 이미지 전체로 번졌다. @@ -73,7 +73,7 @@ PDF 노트의 `{e:#}` 도 `ocr_engine_failure_surfaces_as_warning` 이 고정한 | `charts/Clickpath_Analysis.png` | 1,116 자 / 87 영역 | | `korean-text/연고한2.jpg` | 1,088 자 | -`Clickpath_Analysis.png` 은 얇은 조각 하나 때문에 **이미 인식된 87 개 영역**을 통째로 잃고 있었다. +`Clickpath_Analysis.png` 이 잘 보여 준다. 이 이미지는 박스를 **119 개** 검출하는데, 수정 전에는 그중 **3 개**를 인식한 시점에 얇은 조각을 만나 세션이 죽고 그 3 개까지 함께 버려져 본문이 0 자였다. 수정 후에는 87 개 영역 1,116 자가 나온다. (초안이 "이미 인식된 87 개 영역을 잃었다" 고 적었는데 이는 틀렸다 — 87 은 수정 **후** 나오는 영역 수이고, 중단 시점에 인식돼 있던 것은 3 개다. 계측으로 확인: `ABORT regions_already_recognized=3 of detected_boxes=119`.) 부작용이 없다는 것도 확인했다: 원래 성공하던 204 장의 인식 글자 수가 **한 장도 변하지 않았다**. 이 수정은 순수 가산이다.