- 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.
```bash
ssh gpu2i 'tmux ls | grep mineru-vllm' # alive? do this FIRST every parse session
# Missing → it is down. Ask GLG/nixos to bring it up. Do NOT start the vLLM from here.
```
- `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.
```bash
./run.sh mineru-parse scanpdf/<book>001.pdf mineru-client/out
# → out/<book>001/vlm/{<book>001.md, _content_list.json, images/}
# MOVE useful artifacts under scanpdf/ for commit (Forgejo):
# <book>001.md, _content_list.json, images/ → scanpdf/work/<book>/mineru/
# DROP heavy *_origin.pdf / *_layout.pdf / *_model.json / *_middle.json
```
## 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`.
```bash
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
# Emits: <book>-mineru.org + .changes.log (every transform, counted)
# + .candidates.log (uncertain char fixes, NOT applied)
# + .merges.log (paragraph-merge decisions; §Paragraph splits)
```
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:
```jsonc
{
"meta": { title, author, date, language, publisher, subject, uid }, // → epub #+keywords
"structure": {
"body_start": "<first chapter title>", // everything before = front matter, cut (preface kept)
"chapters": [ { "num": "1장", "title": "..." }, ... ],
"chapter_title_variants": { "<ocr/dash variant>": "<canonical>" },
"back_matter": ["참고문헌", "찾아보기"] // become * level; 찾아보기 internal headings dropped
},
"safe_regex": [ { pattern, replace, desc } ], // AUTO. Must be PROVABLY safe (see strategy).
"literal": [ { from, to, desc } ], // AUTO exact-string fixes (heading OCR, LaTeX).
"candidate_regex": [ { pattern, desc } ], // LOG ONLY (.candidates.log). Never edits body.
"paragraph_merge": { // OPT-IN supervised merge
"enabled": false, // off until validated per book
"categories": ["page_boundary", "samepage_break"], // never eq/image/table by default
"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.
عرض على GitHub