scan, split, OCR, prepare for LLM
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
Evgeniy Ierusalimov a154a1627f updated PaddleOCR with PPv6, introduced helping param --main-lang, remove local llm qwen2.5:7b due to hallucinations 3日前
.ai renamed inage_split to image_prepare, fixed some code and parameters 5日前
src updated PaddleOCR with PPv6, introduced helping param --main-lang, remove local llm qwen2.5:7b due to hallucinations 3日前
tests updated PaddleOCR with PPv6, introduced helping param --main-lang, remove local llm qwen2.5:7b due to hallucinations 3日前
.env.dist pipeline optimization, new parameters (see README.md) 5日前
.gitignore initial release with base split functionality 1週間前
3.md OCR script with engines SuryaOCR and PaddleOCR 5日前
3_1200_02.md OCR script with engines SuryaOCR and PaddleOCR 5日前
README.md updated PaddleOCR with PPv6, introduced helping param --main-lang, remove local llm qwen2.5:7b due to hallucinations 3日前
STAGE2_RESEARCH_RESULT.md OCR script with engines SuryaOCR and PaddleOCR 5日前
installation.md updated PaddleOCR with PPv6, introduced helping param --main-lang, remove local llm qwen2.5:7b due to hallucinations 3日前
requirements.txt updated PaddleOCR with PPv6, introduced helping param --main-lang, remove local llm qwen2.5:7b due to hallucinations 3日前
stage1.md initial release with base split functionality 1週間前
stage2_OCR_updates.md OCR script with engines SuryaOCR and PaddleOCR 5日前
stage2_OCR_updates2.md OCR script with engines SuryaOCR and PaddleOCR 5日前
stage2_research.md OCR script with engines SuryaOCR and PaddleOCR 5日前
stage2_results.md updated PaddleOCR with PPv6, introduced helping param --main-lang, remove local llm qwen2.5:7b due to hallucinations 3日前

README.md

scan2html — Offline Document OCR Pipeline

Полностью офлайн-конвейер обработки отсканированных документов: разрезание изображений на страницы → OCR → структурированный Markdown.


Цель проекта

Преобразование больших технических документов (JPEG/TIFF/PNG), полученных после печати и сканирования, в структурированный Markdown с формулами LaTeX, пригодный для:

  • просмотра в браузере (HTML + MathJax)
  • анализа LLM
  • конвертации в LaTeX/PDF

Полностью offline. Без облачных сервисов.


Pipeline

JPEG/TIFF/PNG (многостраничный скан)
    │
    ▼
src/image_prepare.py        ← Stage 1: подготовка (разрезание, обрезка)
    │ --slice / --slice-auto
    │ --pre-rotate, --border
    │ --post-crop
    │
    ▼
JPEG страницы
    │
    ▼
src/image_ocr.py           ← Stage 2: OCR + Markdown
    │ --ocr-engine paddle|surya
    │ --input file.jpg         (одна страница)
    │ --input pages/           (пакетный режим: все изображения в каталоге)
    │ --llm, --vlm, --force-ocr
    │ --pause 5 (по умолчанию)
    │ --result-one-document    (по умолчанию ON, объединить в один .md)
    ├─ PP-StructureV3 / Surya 2    (OCR)
    ├─ fixups                      (VaR, V^2)
    ├─ VLM corrector               (Qwen3.8 Max, API)
    │
    ▼
.md + .json + .html

Параметры

Stage 1: src/image_prepare.py

Параметр Описание По умолчанию
--input / -i Путь к входному изображению required
--slice / -s Сетка <cols>:<rows>
--slice-auto / -a Автоопределение сетки
--output-dir / -o Каталог для страниц .
--pre-rotate / -r Поворот: 90, 180, 270
--border / -b Белая рамка (px) 5
--unwarp / -u Выпрямить перспективные искажения (Canny → 4-угольный контур → warp) OFF
--denoise Убрать шум сканера (Non-Local Means) OFF
--post-crop / --no-post-crop Обрезка по контенту ON

Пример:

python -m src.image_prepare -i scan.jpg -s 3:3 -b 10 -o pages/
python -m src.image_prepare -i scan.jpg --slice-auto
python -m src.image_prepare -i photo.jpg --unwarp -s 2:2 -b 10
python -m src.image_prepare -i old_scan.jpg --denoise -s 2:2 -b 10

--help

 Usage: python -m src.image_prepare [OPTIONS]

 Подготовить отсканированное изображение: разрезать по сетке, обрезать,
 добавить рамку.

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --input       -i                    <file>              Путь к входному      │
│                                                         изображению (JPEG,   │
│                                                         PNG, TIFF)           │
│ --slice       -s                    <str>               Размер сетки в       │
│                                                         формате              │
│                                                         <колонки>:<строки>,  │
│                                                         например 3:2         │
│ --slice-auto  -a                                        Автоматически        │
│                                                         определить размер    │
│                                                         сетки по содержимому │
│                                                         изображения          │
│ --output-dir  -o                    <directory>         Каталог для          │
│                                                         сохранения           │
│                                                         результатов (по      │
│                                                         умолчанию — текущий) │
│                                                         [default: .]         │
│ --pre-rotate  -r                    <int>               Поворот изображения  │
│                                                         перед разрезанием:   │
│                                                         90, 180 или 270      │
│ --border      -b                    <int range> [x>=0]  Отступ в пикселях    │
│                                                         (белая рамка /       │
│                                                         отступ при обрезке   │
│                                                         содержимого, по      │
│                                                         умолчанию 5)         │
│                                                         [default: 5]         │
│ --unwarp      -u                                        Выпрямить            │
│                                                         перспективные        │
│                                                         искажения перед      │
│                                                         разрезанием          │
│ --denoise                                                Убрать шум сканера  │
│                                                         (Non-Local Means)   │
│                                                         перед разрезанием   │
│ --post-crop   -c  --no-post-crop                        Обрезать каждую      │
│                                                         страницу по границам │
│                                                         полезного            │
│                                                         содержимого          │
│                                                         (включено по         │
│                                                         умолчанию)           │
│                                                         [default: post-crop] │
│ --help                                                  Show this message    │
│                                                         and exit.            │
╰──────────────────────────────────────────────────────────────────────────────╯

Stage 2: src/image_ocr.py

Параметр Описание По умолчанию
--input / -i Файл изображения или каталог required
--output-dir / -o Каталог вывода рядом с --input
--ocr-engine Обязательный: paddle, surya или external-qwen3
--fix-by-external-qwen3 Исправить ошибки VLM API (Qwen3.8 Max) OFF
--main-lang Основной язык: ru (eslav) или en (PP-OCRv6 rec) ru
--pause Пауза между изображениями (сек) 5
--no-result-one-document Не объединять .md в один документ
--no-post-crop Не обрезать по контенту (Stage 1)

Пример:

# Одна страница — PaddleOCR
python -m src.image_ocr -i page.jpg --ocr-engine paddle

# Одна страница — Surya 2
python -m src.image_ocr -i page.jpg --ocr-engine surya

# Пакетный режим
python -m src.image_ocr -i ./pages/ --ocr-engine surya

# Внешняя VLM как OCR-движок (Qwen3.8 Max)
python -m src.image_ocr -i page.jpg --ocr-engine external-qwen3

# Без объединения в один документ
python -m src.image_ocr -i ./pages/ --ocr-engine surya --no-result-one-document

# С VLM-корректором
python -m src.image_ocr -i page.jpg --ocr-engine paddle --fix-by-external-qwen3

# Английский документ (PP-OCRv6 rec)
python -m src.image_ocr -i page.jpg --ocr-engine paddle --main-lang en

--help

 Usage: python -m src.image_ocr [OPTIONS]

 OCR → PostProcess → Markdown. Принимает файл или каталог изображений.

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ *  --input               -i      <path>                Путь к входному       │
│                                                        изображению или       │
│                                                        каталогу (JPEG, PNG,  │
│                                                        TIFF)                 │
│                                                        [required]            │
│ *  --ocr-engine                  <str>                 OCR-движок: paddle    │
│                                                        (PP-StructureV3),     │
│                                                        surya (Surya 2 VLM)   │
│                                                        или external-qwen3    │
│                                                        (Qwen3.8 Max API)     │
│                                                        [required]            │
│    --output-dir          -o      <directory>           Каталог для           │
│                                                        сохранения            │
│                                                        результатов (по       │
│                                                        умолчанию — рядом с   │
│                                                        входным файлом)       │
│    --fix-by-external-q…                                Исправить OCR-ошибки  │
│                                                        внешней VLM (Qwen3.8  │
│                                                        Max, API). Не         │
│                                                        работает с            │
│                                                        --ocr-engine          │
│                                                        external-qwen3        │
│    --no-result-one-doc…                                Не объединять .md     │
│                                                        результаты в один     │
│                                                        документ              │
│                                                        [default: True]       │
│    --pause                       <int range>           Пауза между           │
│                                  [0<=x<=600]           изображениями в       │
│                                                        секундах (по          │
│                                                        умолчанию 5)          │
│                                                        [default: 5]          │
│    --help                                              Show this message and │
│                                                        exit.                 │
╰──────────────────────────────────────────────────────────────────────────────╯

Debug-лог

Для отладки зависаний и падений — файл с полным логом всех этапов обработки. В .env добавить:

OCR_LOG_FILE=image_ocr.log

Формат вывода: ЧЧ:ММ:СС [LEVEL] сообщение. Файл пишется рядом со скриптом или по указанному пути. Уровень DEBUG показывает: начало/конец OCR, сохранение JSON/MD, вызов корректоров.

Переменные окружения (.env)

Скопируй шаблон и заполни своими ключами:

cp .env.dist .env
# Отредактируй .env — вставь свои API-ключи
Переменная Назначение Где взять
OPENCODE_API_KEY VLM-корректор через OpenCode Go opencode.ai/auth → скопировать ключ
HF_TOKEN Скачивание моделей с HuggingFace (Surya 2) huggingface.co/settings/tokens (read-only)
OCR_LOG_FILE Путь к debug-логу (необязательно) Например: image_ocr.log

.env уже добавлен в .gitignore — не коммитится.


Результат

CLI (image_ocr.py) генерирует два файла для каждого изображения:

Файл Формат Описание
page_01.md Markdown Текст + $$-формулы + ##-заголовки. Готов к загрузке в LLM.
page_01.json JSON Полный дамп OCR: bbox, confidence, labels, formulas

При пакетной обработке (каталог на входе) — дополнительно:

Файл Описание
<dirname>.md Объединённый многостраничный Markdown (по умолчанию)
<dirname>.tex LaTeX для всех страниц (через json_to_latex.py)

Ручные утилиты:

Файл Утилита Назначение
.tex src/latex/json_to_latex.py JSON (Paddle / Surya / Qwen) → LaTeX + HTML (автодетект)
# JSON → LaTeX (один файл, автоопределение формата)
python -m src.latex.json_to_latex page.json

# JSON → LaTeX + HTML (каталог — объединённый многостраничный)
python -m src.latex.json_to_latex cbr_en_2/

# Программный вызов
python -c "from src.latex.json_to_latex import json_to_latex; \
  open('page.tex','w').write(json_to_latex('page.json'))"

python -c "from src.latex.json_to_latex import multi_json_to_latex; \
  multi_json_to_latex('out/', 'out/document.tex')"

Качество OCR

Движок Русский текст Формулы Скорость (CPU)
PaddleOCR (PP-StructV3) ~85% ✅ отлично ~140s
+ VLM (Qwen3.8 Max) ~98% ✅✅ +10s/блок
Surya 2 ~95% ✅ отлично ~300s

Передача в LLM (Gemini, Qwen, ChatGPT)

Рекомендуемый формат: Markdown (.md). Это основной формат вывода CLI.

Формат Для LLM Причина
.md ⭐⭐⭐⭐⭐ Нативный для всех LLM. $$-формулы, ##-заголовки — структура сохраняется. Генерируется CLI.
.json ⭐⭐⭐⭐ Полный дамп OCR. Для программной обработки. Генерируется CLI.
.html ⭐⭐⭐ LLM читает HTML, но тратит токены на тэги. Генерируется утилитами.

Пример запроса к LLM после загрузки .md:

Проанализируй раздел 7.1.10 и объясни формулу $$LGD\_extr_i = \max(...)$$

Оптимизации Surya 2 для CPU

Surya 2 использует llama-server (llama.cpp) для инференса на CPU. Настройки по умолчанию рассчитаны на GPU-сервер с параллельными запросами. На одном ноутбучном CPU они избыточны и замедляют работу.

1. Keep-alive сервера (SURYA_INFERENCE_KEEP_ALIVE=true)

По умолчанию llama-server запускается и останавливается при каждом вызове OCR (старт — 23 секунды). Keep-alive держит сервер живым между запросами.

Эффект: -23s при повторных вызовах.

2. Parallel slots (SURYA_INFERENCE_PARALLEL=1)

Количество параллельных «слотов» — сколько запросов llama-server может обрабатывать одновременно. По умолчанию 8 — нужно для сервера с множеством клиентов. Для одной локальной страницы достаточно одного.

Эффект: -80% памяти KV-кэша, меньше переключений контекста.

3. CTX per slot (SURYA_INFERENCE_CTX_PER_SLOT=8192)

Размер контекстного окна в токенах на один слот. Одна страница A4 при 96 DPI генерирует ~2000-2500 токенов на выходе Surya 2. Значение 8192 даёт трёхкратный запас, покрывая самые плотные страницы. По умолчанию 12288 — рассчитано на GPU-сервер с большим VRAM.

Как определено: замерено на 3_1200_02.jpgmax_tokens в запросе ~2400. Утроение — стандартный safety-фактор для VLM (input tokens + output tokens). 8192 = 3 × ~2400.

Эффект: -50% памяти KV-кэша относительно 16384 (= 12288 × 8 / 8 → 12288).

4. Pre-scale до 96 DPI (_IMAGE_MAX_WIDTH=1056px)

Surya 2 обучен на данных с 96 DPI. Ширина A4 при 96 DPI ≈ 794 px, при альбомной ориентации ≈ 1056 px. Полноразмерные сканы (2552×3417 px, ~300 DPI) дают избыточную детализацию, которую vision-энкодер Surya всё равно сжимает до своего внутреннего разрешения. Предварительное сжатие экономит время на передачу и энкодинг без влияния на качество OCR.

Документация Surya подтверждает: «Try going from 192 to 96 for improved throughput» — снижение DPI рекомендовано разработчиками как способ ускорения без потери точности.

Эффект: -60% пикселей на входе, +20-30% скорости vision-энкодера.

5. Threads match CPU (-t 8 --threads-batch 8)

llama-server не указывает количество потоков — по умолчанию берёт все доступные. Явное указание -t и --threads-batch гарантирует оптимальное использование всех 8 ядер без оверхеда на hyperthreading.

Эффект: стабильная загрузка CPU, без деградации при гипертрединге.

Совокупный прирост

Метрика До оптимизации После Прирост
Время холодного старта ~430s ~300s -30%
RAM на KV-кэш ~8 GB ~1 GB -87%
Повторный запуск +23s (перестарт сервера) 0s (keep-alive) -23s

Настройки заданы в движке (src/ocr/surya_engine.py), переопределяются через переменные окружения.


Архитектура

src/
  image_prepare.py      — CLI Stage 1
  image_ocr.py          — CLI Stage 2
  split/slicer.py        — разрезание, post-crop
  ocr/
    paddle_engine.py     — PP-StructureV3
    surya_engine.py      — Surya 2
  postprocess/
    fixups.py            — OCR-ошибки (VaR)
    corrector.py         — Corrector protocol
    vlm_corrector.py     — Qwen3.8 Max (API)
  markdown/generator.py  — Markdown + валидация
  latex/
    json_to_latex.py     — Единый JSON→LaTeX+HTML (Paddle + Surya + Qwen)

Не входит в проект

  • GUI
  • Docker
  • PDF (на вход)
  • LLM/RAG
  • Поиск по документам
  • Многопоточность
  • Автоматический deskew

Лицензия

MIT. Модели PaddleOCR — Apache 2.0, Surya 2 — OpenRAIL-M.