κ³ κΈ PDF λ¬Έμ νμ± μμ€ν β Layout Detection + OCR + Vision Language Model(VLM)μ κ²°ν©νμ¬ PDFλ₯Ό μ§λ₯ν ꡬ쑰ν λ¬Έμλ‘ λ³ν
볡μ‘ν λ μ΄μμμ PDF λ¬Έμλ₯Ό μλμΌλ‘ μ΄ν΄νκ³ κ΅¬μ‘°νλ MarkdownμΌλ‘ λ³νν©λλ€.
- π μ νν λ μ΄μμ λΆμ β YOLO κΈ°λ° μμ κ°μ§
- π€ κ³ νμ§ ν μ€νΈ μΆμΆ β λ₯λ¬λ OCR
- π¨ μ΄λ―Έμ§ μλ ν΄μ β μ°¨νΈ/κ·Έλν λΆμ
- π§ μ§λ₯ν μ½κΈ° μμ β μμ°μ€λ¬μ΄ λ¬Έμ νλ¦
- β‘ κ³ μ±λ₯ λ³λ ¬ μ²λ¦¬ β ThreadPoolExecutor νμ©
PDF νμ΄μ§ μ΄λ―Έμ§ β DocLayout-YOLO β μμ λΆλ₯ λ° μμΉ νμ
μν :
- κ° νμ΄μ§μμ μλ―Έμλ λͺ¨λ μμ μλ κ°μ§
- κ°μ§ μμ: μ λͺ©, λ³Έλ¬Έ, ν, μ°¨νΈ, μ΄λ―Έμ§, μ£Όμ, μΊ‘μ λ±
- κ° μμμ Bounding Box(μ’ν), νμ , μ λ’°λ μ°μΆ
μΆλ ₯ μμ:
{
"type": "figure",
"bbox": {"left": 100, "top": 200, "right": 500, "bottom": 400},
"confidence": 0.95
}κ°μ§λ μμ μμΉ β K-means μ»¬λΌ ν΄λ¬μ€ν°λ§ β μμ°μ€λ¬μ΄ μμ κ²°μ
μκ³ λ¦¬μ¦:
- Yμ’ν κΈ°λ° ν΄λ¬μ€ν°λ§ β ν(row) μλ κ°μ§
- ν λ΄ μ λ ¬ β Xμ’νλ‘ μ’βμ° μ λ ¬
- ν κ° μ λ ¬ β μβμλ μμ νμ
λ€λ¨ λ μ΄μμ μ²λ¦¬:
μ λ¬Έ/μ‘μ§ 2λ¨ λ μ΄μμ:
[μΌμͺ½ μλ¨] β [μ€λ₯Έμͺ½ μλ¨] β [μΌμͺ½ νλ¨] β [μ€λ₯Έμͺ½ νλ¨]
μ΄λ―Έμ§ μμ β DeepSeek-OCR (Ollama) β κ³ μ νλ ν
μ€νΈ
νΉμ§:
- νκΈ/μλ¬Έ νΌν© λ¬Έμ μλ²½ μ§μ
- ν 루μλ€μ΄μ νν°λ§ β λΆμ νν OCR κ²°κ³Ό μλ μ κ±°
- ν¬λ§· μ μ§ β ν, μ½λ λΈλ‘ λ± λ§ν¬λ€μ΄ ν¬λ§· 보쑴
μ°¨νΈ/κ·Έλν μ΄λ―Έμ§ β Gemma3/Qwen3-VL β μλ ν΄μ λ° μμ½
λΆμ νλͺ©:
- μμ½ β μ΄λ―Έμ§μ ν΅μ¬ λ΄μ© μμ½
- ν΅μ¬ ν¬μΈνΈ β μ£Όμ μμΉ λ° νΈλ λ
- μ£Όμμ¬ν β νΉλ³ν μ£Όλͺ©ν μ
μΆλ ₯ ꡬ쑰:
- μμ½: κΈλ‘λ² μΈνλ μ΄μ
μ 2022λ
μ μ μ μ°μ ν νλ½ μΆμΈ...
- ν΅μ¬:
1. 2022λ
8% μ΄κ³Ό
2. 2024λ
3% μμ€ μμ
- μ£Όμ: λ―Έκ΅ μ μ±
λ³ν μ£Όμ νμ
μ¬λ¬ μμμ OCR/VLM μμ
β ThreadPoolExecutorλ‘ λμ μ€ν
μΈ κ°μ§ μ€ν λͺ¨λ:
| λͺ¨λ | νΉμ§ | μ¬μ© μμ |
|---|---|---|
| Parallel | λͺ¨λ OCR/VLM λμ μ€ν | κ³ μ μ²λ¦¬ νμ, μΆ©λΆν λ©λͺ¨λ¦¬ |
| Grouped | OCR μλ£ ν VLM | λ©λͺ¨λ¦¬ ν¨μ¨μ± μ°μ |
| Interleaved | μμλ³ OCRβVLM μμ°¨ | κ· νμ‘ν μ±λ₯ |
β μ νν λ μ΄μμ λΆμ β DocLayout-YOLOλ‘ ν μ΄λΈ, ν μ€νΈ, μ°¨νΈ, μ΄λ―Έμ§ μλ κ°μ§ β κ³ νμ§ OCR β DeepSeek-OCR (Ollama) κΈ°λ° νκΈ/μλ¬Έ νΌν© λ¬Έμ μ§μ β μ§λ₯ν VLM ν΄μ β Gemma3/Qwen3-VLλ‘ μ°¨νΈΒ·κ·Έλ¦ΌΒ·λν μλ λΆμ λ° μμ½ β μμ°μ€λ¬μ΄ μ½κΈ° μμ β K-means κΈ°λ° λ€λ¨ λ μ΄μμ μ²λ¦¬ β κ³ μ±λ₯ λ³λ ¬ μ²λ¦¬ β ThreadPoolExecutorλ‘ OCR/VLM μμ λμ μ€ν
PDF νμΌ
β
βββΊ [Step 1] PDF λ λλ§
β ββ pypdfium2λ‘ νμ΄μ§ μ΄λ―Έμ§ μμ±
β ββ ν
μ€νΈ λ μ΄μ΄ μΆμΆ
β
βββΊ [Step 2] λ μ΄μμ κ°μ§
β ββ DocLayout-YOLOλ‘ μμ κ°μ§
β ββ BBox + νμ
+ μ λ’°λ μ°μΆ
β
βββΊ [Step 3] κ°μ§ κ²°κ³Ό νν°λ§
β ββ μ λ’°λ μκ³κ° νν°λ§
β ββ IoU κΈ°λ° μ€λ³΅ μ κ±°
β
βββΊ [Step 4] μ½κΈ° μμ μΆλ‘
β ββ K-means ν΄λ¬μ€ν°λ§
β ββ μμ°μ€λ¬μ΄ μμ κ²°μ
β
βββΊ [Step 5] OCR/VLM μ²λ¦¬ (λ³λ ¬/μμ°¨)
β ββ OCR: ν
μ€νΈ μμ μΆμΆ
β ββ VLM: μ΄λ―Έμ§ λΆμ
β ββ Ollama μλ² νΈμΆ
β
βββΊ [Step 6] ν
μ€νΈ μ μ
β ββ ν 루μλ€μ΄μ
νν°λ§
β ββ HTML β Markdown λ³ν
β
βββΊ [Step 7] μΆλ ₯ μμ±
ββ out.md (λ§ν¬λ€μ΄)
ββ out.meta.json (λ©νλ°μ΄ν°)
ββ pages/ (λ λλ§ μ΄λ―Έμ§)
ββ crops/ (VLMμ© μ΄λ―Έμ§)
ββ detections/ (κ°μ§ κ²°κ³Ό μκ°ν)
DocLayout-YOLOκ° PDF νμ΄μ§μμ μλμΌλ‘ μμλ₯Ό κ°μ§ν κ²°κ³Ό:
κ°μ§λ μμλ€:
title(0.93) - νμ΄μ§ μ λͺ©plain_text(0.96) - λ³Έλ¬Έ ν μ€νΈfigure(0.96, 0.82, 0.81) - μ°¨νΈ/κ·Έλν μμfigure_caption(0.86, 0.68, 0.58) - κ·Έλ¦Ό μΊ‘μ table_footnote(0.82) - μΆμ²/κ°μ£Όabandon(0.65, 0.93, 0.90) - 무μν μμ (λ‘κ³ , νμ΄μ§ λ²νΈ λ±)
VLM ν΄μ κ²°κ³Ό:
- μμ½: κΈλ‘λ² μΈνλ μ΄μ
μ 2022λ
μ μ μ μ°μ ν λν μΆμΈμ΄λ©°,
2025λ
κΉμ§ νν₯ μμ νλ κ²μΌλ‘ μ λ§λ©λλ€.
- ν΅μ¬:
1. 2022λ
κΈλ‘λ² μΈνλ μ΄μ
μ 8%λ₯Ό λλ μ΅κ³ μ μ κΈ°λ‘
2. 2024λ
μ΄ν μΈνλ μ΄μ
μ 3% μμ€μΌλ‘ νλ½ μμ
3. μμμ μ ν₯κ΅ > μ λ½ μ ν₯κ΅ > κΈλ‘λ² μμΌλ‘ μΈνλ μ΄μ
μμ€
- μ£Όμ: 2025λ
λ―Έκ΅μ μ μ±
λ³νκ° μ£Όμκ΅ κ²½μ μ μν₯ κ°λ₯
VLM ν΄μ κ²°κ³Ό:
- μμ½: 2025λ
κΈλ‘λ² κ²½μ μ±μ₯λ₯ μ μ£Όμκ΅μ ν΅μμ μ±
λ³ν λμμ λ°λΌ
μ°¨λ³νλ κ²μΌλ‘ μμλ©λλ€.
- ν΅μ¬:
- μ€κ΅: 5.2% β 4.8% β 4.5% (νλ½ μΆμΈ)
- λ―Έκ΅: 2.9% β 2.8% β 2.2%
- νκ΅: 1.4% β 2.2% β 2.0%
- μ£Όμ: κ΅κ°λ³ κ²½μ μ±μ₯ μ λ§μΉμ ν° μ°¨μ΄ μ‘΄μ¬
# Parsed Document
## Page 8
κΈλ‘λ² κ²½μ β '25λ
κΈλ‘λ² μ μ±μ₯Β·μΈνλ νν₯ μμ ν κ΅λ©΄ μ λ―Έκ΅ μμΈμ£Όμ
'24λ
μ°μ΄ μ λ§ λλΉ λ―Έκ΅κ³Ό μ€κ΅ κ²½μ 건쑰ν μ±μ₯μΈ μννλ©΄ μ λ°μ μΌλ‘
μΈνλ μ΄μ
νν₯ μμ ν. '25λ
λ―Έκ΅ μμΈμ£Όμκ° μμλλ λ°λ©΄ μ£Όμκ΅ κ²½μ λ
κΈλ‘λ² λ¬΄μνκ²½ λ³νμ ν΅νμ μ±
λΆνμ€μ±μ λν λμλ ₯μ λ°λ₯Έ μ°¨λ³ν μ λ§
## κΈλ‘λ² μΈνλ μ΄μ
μΆμ΄ λ° μ λ§

**μ΄λ―Έμ§ ν΄μ:**
- μμ½: κΈλ‘λ² μΈνλ μ΄μ
μ 2022λ
μ μ μ μ°μ ν λν μΆμΈ...
- ν΅μ¬: ...
- μ£Όμ: ...βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ollamaparser β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Input Layer: β
β ββ pdf_render.py βββΊ Pages β Images β
β ββ detectors.py βββΊ Layout Detection β
β ββ detection_filter.py βββΊ Filter & Clean β
β β
β Processing Layer: β
β ββ layout_order.py βββΊ Reading Order (K-means) β
β ββ ocr_vlm.py βββΊ Task Execution (Parallel/Seq) β
β ββ providers/ βββΊ Ollama Integration β
β β
β Output Layer: β
β ββ text_clean.py βββΊ Clean Text β
β ββ markdown_writer.py βββΊ Generate Markdown β
β ββ metadata.py βββΊ Generate Metadata β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PDF File
β [pdf_render.py]
βββ PageAsset[] {image, text, size}
β [detectors.py]
βββ DetectedElement[] {bbox, type, confidence}
β [detection_filter.py]
βββ Filtered Elements {confidence > threshold}
β [layout_order.py]
βββ Sorted Elements {reading order}
β [ocr_vlm.py]
βββ OCR Results {text}
βββ VLM Results {interpretation}
β [text_clean.py]
βββ Cleaned Text
β [markdown_writer.py]
βββ out.md, out.meta.json
| νμΌ | μν |
|---|---|
types.py |
PageAsset β λ λλ§λ νμ΄μ§ (μ΄λ―Έμ§ κ²½λ‘, ν¬κΈ°, ν
μ€νΈ) |
layout_types.py |
BBox, DetectedElement β κ°μ§ κ²°κ³Ό (μ’ν, νμ
, μ λ’°λ) |
| νμΌ | μν | κΈ°λ₯ |
|---|---|---|
pdf_render.py |
λ λλ§ | PDFium κΈ°λ° κ³ ν΄μλ μ΄λ―Έμ§ λ³ν |
νλ‘μΈμ€:
render_pdf_pages(pdf_path, dpi=150)
ββ κ° νμ΄μ§λ₯Ό μ΄λ―Έμ§λ‘ λ³ν
ββ ν
μ€νΈ λ μ΄μ΄ μΆμΆ (κ°λ₯ν κ²½μ°)
ββ PageAsset[] λ°ν| νμΌ | μν | κΈ°λ₯ |
|---|---|---|
detectors.py |
κ°μ§ | DocLayout-YOLO κΈ°λ° μμ κ°μ§ |
detection_filter.py |
νν°λ§ | IoU μ€λ³΅ μ κ±°, μ λ’°λ νν°λ§ |
νν°λ§ μκ³ λ¦¬μ¦:
1. μ λ’°λ νν°λ§ (confidence < threshold μ κ±°)
2. IoU κΈ°λ° μ€λ³΅ μ κ±° (κ²ΉμΉ¨ > threshold μ λμ μ λ’°λλ§ μ μ§)
3. μ£Όλ³ μ»¨ν
μ€νΈ μΆμΆ (VLM μ
λ ₯μ©)| νμΌ | μν |
|---|---|
layout_order.py |
K-means κΈ°λ° ν ν΄λ¬μ€ν°λ§ λ° μμ κ²°μ |
μκ³ λ¦¬μ¦:
1λ¨κ³: Yμ’ν β K-means ν΄λ¬μ€ν°λ§ (ν κ·Έλ£Ήν)
2λ¨κ³: κ° ν λ΄ Xμ’ν μ λ ¬ (μ’βμ°)
3λ¨κ³: ν κ° μμ νμ (μβμλ)
| νμΌ | μν |
|---|---|
ocr_vlm.py |
TaskExecutor β λ³λ ¬/μμ°¨ μμ μ€ν |
μ€ν λͺ¨λ:
- execute_parallel() βββΊ λͺ¨λ μμ
λμ μ€ν (λΉ λ¦)
- execute_sequential() βββΊ OCRβVLM μμ°¨ (λ©λͺ¨λ¦¬ ν¨μ¨)
- execute_interleaved() βββΊ μμλ³ μμ°¨ (κ· ν)| νμΌ | μν |
|---|---|
providers/base.py |
Protocol μ μ (OCRModel, VisionLanguageModel) |
providers/ollama/client.py |
Ollama HTTP ν΄λΌμ΄μΈνΈ |
providers/ollama/ocr.py |
OCR ꡬν (DeepSeek-OCR μ§μ) |
providers/ollama/vlm.py |
VLM ꡬν (Gemma3/Qwen3-VL μ§μ) |
Ollama ν΅μ :
Python Client β HTTP /api/generate β Ollama Server
ββ deepseek-ocr (OCR_MODEL)
ββ gemma3:27b (VLM_MODEL)
ββ qwen3-vl:30b (VLM_MODEL)
λͺ¨λΈ ꡬμ±:
OCR_MODELνκ²½ λ³μλ‘ OCR λͺ¨λΈ λ³κ²½ κ°λ₯VLM_MODELνκ²½ λ³μλ‘ VLM λͺ¨λΈ λ³κ²½ κ°λ₯.envνμΌμμ κΈ°λ³Έκ° μ€μ κ°λ₯
| νμΌ | μν |
|---|---|
text_clean.py |
ν μ€νΈ μ μ (ν 루μλ€μ΄μ νν°, HTMLβMD) |
quality_check.py |
νμ§ λΆμ (κ²ΉμΉ¨, λΉ μμ λ±) |
| νμΌ | μν |
|---|---|
markdown_writer.py |
Markdown λΉλ (final/debug μ€νμΌ) |
metadata.py |
λ©νλ°μ΄ν° JSON μμ± |
# κΈ°λ³Έ μ€μΉ
uv sync
# DocLayout-YOLO ν¬ν¨ μ€μΉ
uv sync --extra detector- Python 3.10+
- Ollama (λ‘컬 LLM μλ²)
# Ollama λͺ¨λΈ λ€μ΄λ‘λ
ollama pull deepseek-ocr
ollama pull gemma3:27b
# λλ
ollama pull qwen3-vl:30bνλ‘μ νΈ λ£¨νΈμ .env νμΌμ μμ±νμ¬ λͺ¨λΈμ ꡬμ±ν μ μμ΅λλ€:
# .env νμΌ μμ
OCR_MODEL=deepseek-ocr:latest
VLM_MODEL=gemma3:27b
OLLAMA_HOST=http://localhost:11434νκ²½ λ³μ:
OCR_MODEL: OCRμ μ¬μ©ν λͺ¨λΈ (κΈ°λ³Έκ°:deepseek-ocr:latest)VLM_MODEL: VLMμ μ¬μ©ν λͺ¨λΈ (κΈ°λ³Έκ°:gemma3:27b)OLLAMA_HOST: Ollama μλ² μ£Όμ (κΈ°λ³Έκ°:http://localhost:11434)
λͺ¨λΈ λ³κ²½ μμ:
# λ€λ₯Έ OCR λͺ¨λΈ μ¬μ©
OCR_MODEL=custom-ocr:v2 uv run ollamaparser-parse --pdf document.pdf --out_dir ./output
# λ€λ₯Έ VLM λͺ¨λΈ μ¬μ©
VLM_MODEL=qwen3-vl:30b uv run ollamaparser-parse --pdf document.pdf --out_dir ./output
# μ격 Ollama μλ² μ¬μ©
OLLAMA_HOST=http://192.168.1.100:11434 uv run ollamaparser-parse --pdf document.pdf --out_dir ./outputuv run ollamaparser-parse \
--pdf "document.pdf" \
--out_dir ./output \
--detector doclayout-yolo \
--yolo-weights "juliozhao/DocLayout-YOLO-DocStructBench" \
--yolo-from-pretrained \
--ocr-provider deepseek-ollama \
--ocr-scope elements \
--vlm-provider ollama-vlm \
--vlm-scope visuals \
--execution-mode parallel| μ΅μ | μ€λͺ |
|---|---|
--detector |
doclayout-yolo / pdfium-text / fallback |
--yolo-weights |
YOLO κ°μ€μΉ κ²½λ‘ λλ HF repo_id |
--yolo-from-pretrained |
HuggingFaceμμ κ°μ€μΉ λ€μ΄λ‘λ |
--save-detections |
κ°μ§ κ²°κ³Ό μ΄λ―Έμ§/JSON μ μ₯ |
| μ΅μ | μ€λͺ |
|---|---|
--ocr-provider |
none / deepseek-ollama |
--ocr-scope |
none / page / elements |
--ocr-mode |
markdown / free |
--figure-ocr |
κ·Έλ¦Ό μμλ OCR μν |
| μ΅μ | μ€λͺ |
|---|---|
--vlm-provider |
none / ollama-vlm |
--vlm-scope |
none / page / visuals |
--ollama-vlm-model |
gemma3:27b / qwen3-vl:30b |
--vlm-use-image |
VLMμ μ΄λ―Έμ§ μ λ¬ |
--figure-vlm |
κ·Έλ¦Ό μμ VLM ν΄μ |
| μ΅μ | μ€λͺ |
|---|---|
--execution-mode |
grouped / interleaved / parallel |
--parallel-workers |
λ³λ ¬ μ컀 μ (κΈ°λ³Έ: 4) |
| μ΅μ | μ€λͺ |
|---|---|
--md-style |
final (κΉλ) / debug (bbox ν¬ν¨) |
--metadata/--no-metadata |
λ©νλ°μ΄ν° JSON μμ± μ¬λΆ |
output/
βββ pages/ # λ λλ§λ νμ΄μ§ μ΄λ―Έμ§
β βββ page_0001.png
β βββ page_0002.png
βββ crops/ # VLMμ© ν¬λ‘ μ΄λ―Έμ§
β βββ page_0001_elem_005.jpg
β βββ page_0002_elem_012.jpg
βββ detections/ # κ°μ§ κ²°κ³Ό (--save-detections)
β βββ page_0001.jpg # λ°μ΄λ©λ°μ€ μ€λ²λ μ΄
β βββ page_0001.json # κ°μ§λ μμ JSON
βββ out.md # μ΅μ’
Markdown μΆλ ₯
βββ out.meta.json # λ©νλ°μ΄ν° (μ ν)
βββ run.log # μ€ν λ‘κ·Έ
pypdfium2- PDF λ λλ§pillow- μ΄λ―Έμ§ μ²λ¦¬typer- CLI νλ μμν¬pydantic- λ°μ΄ν° κ²μ¦requests- Ollama API νΈμΆscikit-learn- K-means ν΄λ¬μ€ν°λ§
doclayout-yolo- DocLayout-YOLO SDKhuggingface-hub- λͺ¨λΈ λ€μ΄λ‘λ
MIT License


