scan, split, OCR, prepare for LLM
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
Evgeniy Ierusalimov cfa2eb868e fix parameter 4日前
.ai polishing the code and docs 5日前
src fix parameter 4日前
tests OCR script with engines SuryaOCR and PaddleOCR 5日前
.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 fix parameter 4日前
STAGE2_RESEARCH_RESULT.md OCR script with engines SuryaOCR and PaddleOCR 5日前
installation.md pipeline optimization, new parameters (see README.md) 5日前
requirements.txt pipeline optimization, new parameters (see README.md) 5日前
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 pipeline optimization, new parameters (see README.md) 5日前

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_split.py          ← Stage 1: разрезание
    │ --slice / --slice-auto
    │ --pre-rotate, --border
    │ --post-crop
    │
    ▼
JPEG страницы
    │
    ▼
src/image_ocr.py           ← Stage 2: OCR + Markdown (бывший image_to_latex.py)
    │ --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)
    ├─ LLM corrector               (Qwen2.5-7B, local)
    ├─ VLM corrector               (Qwen3.8 Max, API)
    │
    ▼
.md + .json + .html

Параметры

Stage 1: src/image_split.py

Параметр Описание По умолчанию
--input / -i Путь к входному изображению required
--slice / -s Сетка <cols>:<rows>
--slice-auto / -a Автоопределение сетки
--output-dir / -o Каталог для страниц .
--pre-rotate / -r Поворот: 90, 180, 270
--border / -b Белая рамка (px) 50
--post-crop / --no-post-crop Обрезка по контенту ON

Пример:

python -m src.image_split -i scan.jpg -s 3:3 -b 10 -o pages/
python -m src.image_split -i scan.jpg --slice-auto

Stage 2: src/image_ocr.py

Параметр Описание По умолчанию
--input / -i Файл изображения или каталог required
--output-dir / -o Каталог вывода рядом с --input
--lang / -l Язык OCR en
--ocr-engine paddle или surya paddle
--llm Локальный LLM-корректор (Qwen2.5-7B) OFF
--vlm Vision LM корректор (Qwen3.8 Max, API) OFF
--force-ocr Перераспознать даже при наличии .json OFF
--pause Пауза между изображениями (сек) 5
--no-result-one-document Не объединять .md в один документ
--no-post-crop Не обрезать по контенту (Stage 1)

Пример:

# Одна страница
python -m src.image_ocr -i page.jpg

# Пакетный режим — все изображения в каталоге
python -m src.image_ocr -i ./pages/ --ocr-engine surya

# Принудительное перераспознавание
python -m src.image_ocr -i ./pages/ --force-ocr

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

# С LLM-корректором
python -m src.image_ocr -i page.jpg -l ru --llm

# С VLM-корректором (нужен OPENCODE_API_KEY в .env)
python -m src.image_ocr -i page.jpg --vlm

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)

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

Файл Утилита Назначение
.html src/latex/surya2html.py Surya JSON → HTML + MathJax
.tex src/latex/json_to_latex.py Paddle или Surya JSON → LaTeX (автодетект)
# JSON → HTML (Surya)
python -c "from src.latex.surya2html import surya_json_to_html; \
  open('page.html','w').write(surya_json_to_html('page.json'))"

# JSON → LaTeX (Paddle или Surya — автоопределение)
python -c "from src.latex.json_to_latex import json_to_latex; \
  open('page.tex','w').write(json_to_latex('page.json'))"

# Многостраничный LaTeX (все 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
+ LLM (Qwen2.5-7B) ~95% +5 min
+ 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_split.py         — CLI Stage 1
  image_ocr.py          — CLI Stage 2 (бывший image_to_latex.py)
  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
    llm_corrector.py     — Qwen2.5-7B (local)
    vlm_corrector.py     — Qwen3.8 Max (API)
  markdown/generator.py  — Markdown + валидация
  latex/
    surya2html.py        — Surya JSON → HTML
    surya2latex.py       — Surya JSON → LaTeX
    json_to_latex.py     — Единый JSON→LaTeX (Paddle + Surya)
    tex2html.py           — TeX → HTML

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

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

Лицензия

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