scan, split, OCR, prepare for LLM
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Evgeniy Ierusalimov 4325ccec9b removed useless parameter 4 päivää sitten
.ai polishing the code and docs 5 päivää sitten
src removed useless parameter 4 päivää sitten
tests OCR script with engines SuryaOCR and PaddleOCR 5 päivää sitten
.env.dist pipeline optimization, new parameters (see README.md) 5 päivää sitten
.gitignore initial release with base split functionality 1 viikko sitten
3.md OCR script with engines SuryaOCR and PaddleOCR 5 päivää sitten
3_1200_02.md OCR script with engines SuryaOCR and PaddleOCR 5 päivää sitten
README.md removed useless parameter 4 päivää sitten
STAGE2_RESEARCH_RESULT.md OCR script with engines SuryaOCR and PaddleOCR 5 päivää sitten
installation.md pipeline optimization, new parameters (see README.md) 5 päivää sitten
requirements.txt pipeline optimization, new parameters (see README.md) 5 päivää sitten
stage1.md initial release with base split functionality 1 viikko sitten
stage2_OCR_updates.md OCR script with engines SuryaOCR and PaddleOCR 5 päivää sitten
stage2_OCR_updates2.md OCR script with engines SuryaOCR and PaddleOCR 5 päivää sitten
stage2_research.md OCR script with engines SuryaOCR and PaddleOCR 5 päivää sitten
stage2_results.md pipeline optimization, new parameters (see README.md) 5 päivää sitten

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
--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/ --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.