PDF OCR 이 DCTDecode 아닌 스캔본을 전량 건너뜀 — 페이지 렌더링으로 교체 (사용자 수동 정규화 제거) #232
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
증상
스캔 PDF 를 ingest 하면 전 페이지가 OCR 없이 건너뛰어지고, 문서가 사실상 빈 내용으로 색인된다. 조용한 내용 손실이다.
전 페이지
0ms→ OCR 엔진은 호출조차 되지 않았다. 엔진 실패라면 실제 latency 가 찍히고failure_reason: Some("ocr_error")가 붙는다(crates/kebab-app/src/pdf_ocr_apply.rs:257,:393).원인
extract_dctdecode_page_image(crates/kebab-parse-pdf/src/page_image.rs:66-76)는/Filter가 정확히DCTDecode(JPEG) 인 image XObject 만 받는다. 주석에 "v1 scope = DCTDecode passthrough only" 로 명시된 의도적 축소 범위다.걸러지는 것:
/FilterCCITTFaxDecodeJBIG2DecodeFlateDecodeJPXDecode[FlateDecode, DCTDecode]체인:69)DCTDecode인데 JPEG magic 없음:82)주목할 점: 텍스트 게이트는 정상 작동했다.
needs_ocr = chars < min_char_count(20) || valid_ratio < 0.5(pdf_ocr_apply.rs:143)를 통과했다는 건 kebab 이 "이 페이지는 스캔본이라 OCR 이 필요하다"고 올바르게 판정했다는 뜻이다. 판정은 맞았고 래스터를 못 꺼냈을 뿐이다.always_on기본값이false(crates/kebab-config/src/lib.rs:826)이므로, 전 페이지 시도 = 전 페이지가 텍스트 추출에 실패했다는 확증이다.사용자 수동 정규화로 두면 안 되는 이유
현재 코드 주석은 사용자가
pdftoppm/ghostscript로 PDF 를 JPEG 기반으로 재래스터화하는 것을 전제한다("see release notes for normalization guidance",pdf_ocr_apply.rs:160). 이건 성립하지 않는다./Filter가 뭔지 알 수 없고, 알아야 할 이유도 없다.IngestReport에 집계되지 않는다. 대량 ingest 에서는 지나간다.kebab ingest)에서 문서별 전처리는 현실적이지 않다.ingest 는 넣은 파일을 알아서 처리해야 한다. 사용자가 PDF 내부 인코딩을 신경 쓰는 순간 도구가 진 것이다.
부수 결함 2건 (같은 코드 경로)
1. 페이지 내 image XObject 선택이 비결정적
for (_name, obj) in xobject.iter()(page_image.rs:44)로 순회하며 처음 만난 DCTDecode 이미지를 반환한다. lopdf 의 dictionary 순회 순서는 보장되지 않으므로:2. CLI 가
failure_reason을 버린다(
crates/kebab-cli/src/progress.rs:357-372) wire 이벤트는 원인을 구분해 실어 보내는데(None= 래스터 부재,Some("ocr_error")= 엔진 실패) 사람이 보는 출력에서 둘을 뭉갠다. #228 과 같은 계열 — 신호는 있는데 표시 단계에서 소실.제안
본안: DCTDecode passthrough 를 페이지 렌더링으로 교체
A 로 가면
page_image.rs의 XObject traversal(~90줄)이 통째로 삭제된다. 추가가 아니라 교체이고 순감이다. 부수 결함 1번(비결정적 선택)도 함께 사라진다.렌더러 선택 주의:
pdfium-render크레이트. 이쪽을 쓸 것.DCTDecode 고속 경로를 남길지: 남기지 말 것을 권한다. 렌더링은 페이지당 수십 ms 수준이라 OCR 호출(초 단위)에 묻힌다. 경로를 둘로 유지하는 값이 없다.
부수
render_dpiconfig 노브 추가 (기본 300). 기존pdf.ocr.max_pixels(config/lib.rs:443)를 상한으로 존중.failure_reason을 CLI 출력에 노출하고, 값에/Filter이름을 실어 원인이 바로 보이게.IngestReport에 집계 (ocr_skipped_pages등) — stderr 한 줄로 흘리지 말 것.구현 시 주의 — parser_version cascade 필요
수정만 하면 이미 색인된 PDF 에는 적용되지 않는다.
try_skip_unchanged(crates/kebab-app/src/ingest.rs:833)는 (내용 해시 + parser/chunker/embedding 버전) 4개가 모두 일치하면Unchanged로 건너뛴다. PDF 파일 자체는 안 바뀌었으니 해시가 같고, parser_version 을 올리지 않으면 재처리가 일어나지 않는다.→ PDF parser_version bump 필수 (또는 사용자가
--force-reingest). CLAUDE.md §Versioning cascade 대상이고, 새 source 형식 처리 추가에 해당하므로 minor bump + 도그푸딩 트리거다.도그푸딩 코퍼스에 필터별 fixture 를 추가할 것:
corpus/pdf/아래ccitt/jbig2/flate/jpx/mixed-logo-and-scan/vector-only.관련
PR #238 로 닫는다.
본안 — 페이지 렌더링
이슈가 권한 안 A(pdfium 페이지 렌더링)로 갔다. 필터를 지원할 것도, XObject 중에 고를 것도 없고, 벡터와 이미지가 섞인 페이지도 리더가 보는 대로 나온다. 부수 결함 1번(image XObject 선택이 비결정적)도 이 경로에서는 성립하지 않는다.
다만 교체가 아니라 '렌더러 우선 + DCTDecode 폴백' 이다. pdfium 은 공유 라이브러리로만 배포되고 정적 빌드가 없어서, 링크하면 CLAUDE.md 가 규정한 단일 바이너리가 깨진다. 사용자와 상의해 런타임 동적 로드로 정했다 — 있으면 전 인코딩 커버, 없으면 예전 동작 + 왜 건너뛰었는지 명시.
[ingest.pdf.ocr] render_library로 경로를 지정하거나 로더 경로에 두면 되고,kebab doctor의pdf_render가 지금 어느 쪽인지 보고한다. 바이너리는 392.9 → 399.3 MB (+6.4 MB 글루),ldd에 pdfium 없음.부수 제안 2·3
failure_reason이 CLI 에서..로 버려지고 있었다. 이제no_renderer/render_error/unopenable_pdf/ocr_error를 구분해 찍는다.IngestReport.ocr_skipped_pages를 추가하고 사람용 요약에도ocr-skipped N으로 낸다.parser_version cascade
pdf-text-v1→pdf-text-v2. 안 올리면 이미 색인된 스캔본은 파일이 안 바뀌었으니 그대로 건너뛴다 — 사용자가--force-reingest를 떠올려야만 고쳐지는 수정은 고쳐진 게 아니다.실측
정답 텍스트를 아는 한국어 합성 픽스처(CCITT 3건, 22쪽)를 config 기본 모델
qwen2.5vl:3b로:22쪽 전부 성공, 건너뜀 0. 이 PR 이전에는 세 문서 모두 본문 0 자였다. 렌더링 자체는 여섯 필터 계열(CCITT/JBIG2/Flate/JPX/혼합/DCT) 전부 확인했다.
구현 중 알게 된 것
이슈가 예상하지 못한 것 셋이 나왔다. pdfium 은 동시 사용이 안전하지 않다 — 테스트를 병렬로 돌리자 double free 로 프로세스가 죽었고,
thread_safe기능만으로는 부족했다. 뮤텍스로 직렬화했다.set_target_width만 주면 긴 스캔에서 C++length_error로 프로세스가 죽는데 exceptions 비활성 빌드라 Err 로 받지도 못한다.set_maximum_*만으로는 스케일이 안 된다 — 클램프일 뿐이라 초안이 72 DPI 로 렌더하고 있었고render_dpi가 아무 일도 안 했다. 리뷰가 잡았다.그리고 범용 멀티모달 모델은 OCR 엔진이 아니다. 같은 픽스처를
gemma3:4b로 읽혔더니 래스터는 정상인데 출력이 원문과 무관��� 환각이었고, 해상도가 올라가자 180초 타임아웃까지 났다. config 기본값이qwen2.5vl:3b인 이유가 이것이다.안 한 것
"DCTDecode 고속 경로를 남기지 말 것" 은 따르지 않았다. 폴백이 곧 그 경로이고, 폴백을 두는 것이 배포 결정의 귀결이다. 렌더러가 있으면 그 경로는 타지 않는다.