| name | scanbook |
| description | Turn a scanned Korean book PDF into a clean EPUB via the MinerU→post-process→org→ox-epub pipeline in ~/repos/gh/memex-kb. Use when working on any book under scanpdf/work/<book>/ — parsing a scan with MinerU, writing/extending a book correction config, fixing OCR misreads, reconstructing heading/footnote/paragraph structure, merging page-split paragraphs, building or validating the EPUB, or starting a NEW book (e.g. 물리학강의, 물리의정석, 자연철학강의, 인공지능시대). Triggers: 'scanbook', 'mineru', '물리학강의', '스캔책', '스캔북', 'epub 만들', 'epub 빌드', 'mineru-parse', 'mineru2org', '책 전사', 'OCR 책', 'org2epub', '용어집 만들', 'diff-review', 'para-splits', '문단 잘림', '문단 봉합', '감독형 봉합'. This skill carries the tribal knowledge run.sh and docs cannot: the remote GPU dependency, the correction strategy, the paragraph-merge judgment, and the hard-won gotchas. |
| user_invocable | true |
scanbook — scanned book → EPUB pipeline
Repo: ~/repos/gh/memex-kb. Book data + outputs live in the nested private repo
scanpdf/ (Forgejo glg-bot/scanpdf). This skill is the operating surface; run.sh
covers the local commands, this file covers the rest (remote GPU server, per-book config,
correction + paragraph-merge judgment, gotchas).
Mental model: MinerU lays down ~95% (chars + layout). The last 5% is judgment — char
misreads and page-split paragraphs. Both are fixed by detect-then-supervise tools,
never by blind blanket automation.
Pipeline
PDF ──①MinerU VLM (REMOTE gpu2i)──▶ md + content_list.json + images/
──②mineru2org.py (post-process)─▶ clean .org (headings/footnotes/eq/images
│ + corrections + supervised paragraph-merge)
──③ox-epub──────────────────────▶ .epub (epubcheck 0/0/0)
QA: ④diff-review · para-splits config: ⑤scripts/corrections/<book>.json
- Stage ① is the only non-local part, not fully expressible in
run.sh.
- vision/Opus full transcription is retired. Do not spin Opus agents on page images.
- Two classes of last-5% work, each with a detect→supervise tool:
- char misreads →
corrections/<book>.json (safe_regex/literal/candidate_regex)
- page-split paragraphs →
para-splits detector + supervised merge (§Paragraph splits)
⚠️ Stage ① — remote MinerU server (run.sh can't own this)
Inference runs on gpu2i (RTX 5080), a vLLM server serving MinerU2.5-Pro on port 30000.
This is nixos 담당's domain — a tmux session, not a memex-kb artifact.
ssh gpu2i 'tmux ls | grep mineru-vllm'
run.sh mineru-parse auto-tunnels localhost:30000 → gpu2i:30000; only needs the
server already running. Health = curl -sf localhost:30000/health (vLLM returns 200 with
empty body — the -sf exit code is truth, not the empty output).
- Client install (one-time):
./run.sh mineru-setup (uv sync; opencv-headless override).
The client (mineru-client/) is a thin http-client; weights live on gpu2i.
- A 261-page book parses in ~3 min. Background it for big books.
./run.sh mineru-parse scanpdf/<book>001.pdf mineru-client/out
OCR engine choice — 4 engines, model layer vs tool layer (measured 2026-06)
The single most important frame: an engine is two layers. Compare within a layer.
- Tool (layout + asset 픽셀 crop + formula LaTeX + structure labels): MinerU pipeline,
PaddleOCR PP-StructureV3. These give what a pure OCR model cannot.
- Model (page→text only): MinerU2.5-VLM, DeepSeek-OCR, PaddleOCR-VL.
| MinerU | PP-StructureV3 | DeepSeek-OCR | PaddleOCR-VL | GLM-OCR | Upstage DP |
|---|
| layer | tool+model | tool | model | model | model | tool+model (SaaS) |
| serving | gpu2i:30000 | gpu3i:8118 (PaddleX /layout-parsing) | gpu1i:8000 | gpu3i:8000 | gpu3i:8101 | 자체 GPU 없음 |
| run.sh | mineru-parse | ppstructure-parse | deepseek-parse | paddleocr-parse | (재측정: paddleocr 클라 --url …:8101 --model glm-ocr --prompt "Text Recognition:") | (미노출, curl) |
| image px crop | ✓ 9 | ✓ 9 (크롭10) | bbox만 | ✗ | ✗ | ✓ 11 (base64) |
| formula LaTeX | ✓ \mathrm{} | ✓ inline까지 | ✓ H_2 | ✗ 평문 2H2 | ✗ | ✗ 평문 + heading 오분류 |
| structure | content_list | parsing_res_list (동급) | grounding(부분) | ✗ | ✗ | elements 5/12종(caption·equation 0) |
| spacing 공백비율 | 정상 | 0.12 붕괴 | 0.21 | 0.23 | 0.22 | 0.242 정상 |
| 한국어 글자 | mosaic/焮 환각 | 보통(mobile-rec) | 우수 | 우수 | 최악 + 한자환각(磊嚣螽) | 최상 · 텍스트층 환각 0(물리학강의 샘플 한정) / chart path 환각 확인 |
⚠️ 이 표는 "2026-06-06 기록 기반 상대비교 + Upstage 1차 스크리닝"이다. 앞 5열은 2026-06-06
기록 인용이고 raw artifact 가 없다(정량 재현성 debt, 재측정은 deferred — NEXT.md 6번).
정량 순위로 인용하지 말 것.
gold 대조 진행분(2026-07-30): 물질생명인간 1~4장 vision 전사 합본을 기준으로 Upstage vs
MinerU 정량 CER 산출 완료 — 단 이건 vision silver 기준이지 adjudicated 아니다. 그리고 그
과정에서 diff_review 정규화 비대칭 4종을 발견해 고쳤다(그전 숫자는 전부 무효). 계측 규칙은
scripts/diff_review.py 의 "대칭 원칙" 주석이 SSOT, 정량 CER 은 ./run.sh cer-eval.
projection 3종을 함께 볼 것 — 각주 포함 여부로 순위가 뒤집힌다.
채택 판정 = GitHub 이슈 #5(체크리스트 SSOT), 포인터는 NEXT.md. 서사·판단 근거는
봇로그 20260606T130306.
★ Upstage Document Parse (2026-07-30 측정, 6번째 엔진, 관리형 API) — POST /v1/document-digitization,
model=document-parse(→document-parse-260128), ocr=force, base64_encoding=["figure"]. 17p 12.4s
(0.73s/p). 키 = ~/.env.local의 UPSTAGE_API_KEY(출력 금지). 글자 층 6엔진 중 1위: 다섯 엔진이
서로 다르게 깨먹던 고유명사(톰슨·돌턴·슈뢰딩거·찐빵)를 전부 맞힌 유일 엔진, MinerU 한자환각
4건 자리 정확(꺌岁月을→꿰뚫을). 단 구조 층은 base 불가: ① 수식 평문화 + heading1 오분류
(문서상 equation 카테고리는 LaTeX 지원 → 기능부재 아닌 분류 실패), ② 2단 캡션이 본문 문장
한가운데를 가름(과학의 아버+그림 5-1: 돌턴+지라고) — caption 카테고리 0건, ③ 인쇄 줄바꿈
그대로 보존(432줄 중 306 지점 어절 분할, 데카르\n트는 → grep 오판 유발; 단 한국어는 붙임표가 없어
공백 없이 join 하면 대개 복원 = 후처리 흡수 가능), ④ heading1 오탐 7건 중 5건(본문조각·수식·캡션).
mode=enhanced는 구조 개선 0(카테고리·오탐 동일), 대신 figure에 영문 VLM 설명 부착(텍스트
12.8k→23.5k) — 정본 금지, 검색색인/alt 용도. 원자료 = scanpdf/work/물리학강의/upstage/README.md.
입력 포맷에 HWP/HWPX 포함(미측정, 후속 대상).
Measured on 물리학강의 5강 (PDF p121–137). Proper-noun failures DON'T overlap —
this is the key: 톰슨 = MinerU 톈슨(+mosaic환각)·Paddle 4변형·PP 통슨·GLM 통속 / DeepSeek ✓;
돌턴 = DeepSeek 돌탄✗·GLM✗전멸 / MinerU·Paddle·PP ✓; 찐빵 = MinerU 6변형·Paddle✗ / DeepSeek·PP ✓.
⚠️ GLM-OCR 함정: OmniDocBench 94.6%(5엔진 중 SOTA 1등)인데 한국어 스캔책에선 꼴찌 —
돌턴/톰슨/볼츠만 정상표기 0건, 한자 환각. 띄어쓰기만 보존. 한국어 책 제외. OmniDocBench
점수 ≠ 한국어 충실도의 결정적 반례. 영문 문서엔 SOTA일 것.
★ PaddleOCR-VL has TWO serving modes — 같은 모델, 다른 출력:
- 모델 모드 (vLLM
/v1/chat/completions, gpu3i:8000/8001): 순수 텍스트만. asset·구조·수식 없음.
- 도구 모드 (PaddleX
/layout-parsing, gpu3i:8119, ppstructure_client.py --url …:8119):
모델+도구 동시 — 텍스트 우수+띄어쓰기 보존(0.202)+asset 12개+구조 9종(vision_footnote/
display_formula/reference_content). 돌턴·볼츠만·굽은 ✓(GLM/DeepSeek/MinerU가 깨먹은 곳 맞힘).
약점: 느림(8.5s/p), 톰슨→통슨. 현 시점 단일 최강 base.
현재 최선 (decision rule, the method):
- Tool base = PaddleOCR-VL 도구모드(:8119) — MinerU 환각도 PP mobile-rec 띄어쓰기붕괴(0.12)도
둘 다 회피하는 단일 최강. MinerU = 빠른 대안 base(asset+content_list, 속도 ↑; 텍스트는
oracle 교정). PP-StructureV3(mobile-rec)는 kime 띄어쓰기 복원 전제라 후순위
(scanbook ↔ textlint-ko 연결점).
- Body char oracle = Upstage
ocr (2026-07-30 갱신, 잠정 — 이슈 #5 gold 검증 전). 역할 분리:
ocr($0.0015/p) = 저가 글자 oracle + word confidence 기반 후보 생성. DP와 글자 결과
사실상 동일(유사도 0.979)한데 1/6.7 가격, 구조는 없음. confidence 최저값이 자기 이물질
(볼Bal츠만·�)을 지목 → candidate 자동생성 축. 이 자리의 1순위.
document-parse($0.01/p) = 산문 문서의 구조+텍스트(heading·각주·목록·표). 산문책
단독 base 후보. 수식·2단 도판 책에서는 구조 base 불가.
chart_recognition=false 필수 — 도식/개념도 문서에서 없는 수치를 생성(칸트 도식
→ 0.08/0.28/0.47). 기본 true이고 mode=enhanced에서는 강제 활성이라 enhanced 자체를 피한다.
- 유료 API이므로 책 전체가 아니라 깨진 span 의 해당 쪽만 뽑아 호출
(
mutool merge -o span.pdf <book>.pdf N-M). 대안(무료·로컬) = DeepSeek(고유명사 강, 단 돌턴✗)
또는 PaddleOCR-VL(환각 없고 본문 깨끗).
→ 정확 토큰 → corrections/<book>.json. Proven: Guq은→굽은, mosaic→톰슨, 짧빵→찐빵, 꺌岁月→꿰뚫.
- 고유명사 = multi-engine voting. 엔진 불일치 토큰 = 오독 의심점 → 교정후보 자동생성
(차기
engine_vote.py). 한 엔진의 반복 오독은 못 잡지만(예: 시간펼침→시간필침 ×14),
엔진 불일치 는 그 자체로 신호. consistency≠accuracy 의 역(逆)활용.
- MEASURE first on one chapter — predictions were wrong repeatedly. 점수표(OmniDocBench)
≠ 한국어 스캔책 고유명사 충실도.
DeepSeek serving: ssh gpu1i 'tmux ls | grep deepseek-ocr'; deepseek-parse auto-tunnels
localhost:8000 → gpu1i:8000. grounding prompt = <image>\n<|grounding|>Convert the document to markdown.
Stage ② — post-process: scripts/mineru2org.py
Deterministic structure-recovery converter (same input → byte-identical output), driven by
the per-book config (--corrections) + --content-list.
python3 scripts/mineru2org.py scanpdf/work/<book>/mineru/<book>001.md \
-o scanpdf/work/<book>/mineru/<book>-mineru.org \
--corrections scripts/corrections/<book>.json \
--content-list scanpdf/work/<book>/mineru/<book>001_content_list.json
Passes (all logged): surface (image ![]()→[[file:]], block $$→\[\], inline $$→\(\),
footnote $^{n}$ + unicode superscript ⁵⁸²³→[fn:n]); HTML cleanup (<details>/mermaid
removed, <table>→org table); structure recovery (chapter * / section ** num+title
merge / subsection *** / false-heading demotion / front-matter+TOC cut / preface kept);
footnote defs from content_list.page_footnote → * 각주 section + orphan numeric
paragraphs absorbed; supervised paragraph-merge (opt-in); corrections; epub header from meta.
Stage ⑤ — the per-book config (the real per-book work)
scripts/corrections/<book>.json. Copy 물질생명인간.json as template:
{
"meta": { title, author, date, language, publisher, subject, uid },
"structure": {
"body_start": "<first chapter title>",
"chapters": [ { "num": "1장", "title": "..." }, ... ],
"chapter_title_variants": { "<ocr/dash variant>": "<canonical>" },
"back_matter": ["참고문헌", "찾아보기"]
},
"safe_regex": [ { pattern, replace, desc } ],
"literal": [ { from, to, desc } ],
"candidate_regex": [ { pattern, desc } ],
"paragraph_merge": {
"enabled": false,
"categories": ["page_boundary", "samepage_break"],
"overrides": [ { "tail": "<suffix>", "seam": "space|nospace|skip" } ]
}
}
Author it by reading the parsed md: heading layout, section numbering, recurring OCR
misreads, index/reference layout.
🎯 Char-correction strategy (hard-won — do not regress)
Central vocabulary is the highest-priority target: a key term mangled in the index is mangled
in the body too.