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 3a0d25126c .raw as non-changeable results of VLM processing, all postprocessing routines are moved to 3rd stagge generate_result; postprocessing upgraded пре 2 недеља
.ai updated discovered self skills пре 3 недеља
src .raw as non-changeable results of VLM processing, all postprocessing routines are moved to 3rd stagge generate_result; postprocessing upgraded пре 2 недеља
tests added SuryaOCR batch mode; some fixes пре 2 недеља
.env.dist pipeline optimization, new parameters (see README.md) пре 3 недеља
.gitignore .raw as non-changeable results of VLM processing, all postprocessing routines are moved to 3rd stagge generate_result; postprocessing upgraded пре 2 недеља
3.md OCR script with engines SuryaOCR and PaddleOCR пре 3 недеља
3_1200_02.md OCR script with engines SuryaOCR and PaddleOCR пре 3 недеља
README.md .raw as non-changeable results of VLM processing, all postprocessing routines are moved to 3rd stagge generate_result; postprocessing upgraded пре 2 недеља
STAGE2_RESEARCH_RESULT.md OCR script with engines SuryaOCR and PaddleOCR пре 3 недеља
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 пре 3 недеља
stage2_OCR_updates.md OCR script with engines SuryaOCR and PaddleOCR пре 3 недеља
stage2_OCR_updates2.md OCR script with engines SuryaOCR and PaddleOCR пре 3 недеља
stage2_research.md OCR script with engines SuryaOCR and PaddleOCR пре 3 недеља
stage2_results.md .raw as non-changeable results of VLM processing, all postprocessing routines are moved to 3rd stagge generate_result; postprocessing upgraded пре 2 недеља

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: подготовка (разрезание, unwarp, denoise)
    │ --slice / --slice-auto
    │ --pre-rotate, --border
    │ --unwarp, --denoise, --post-crop
    │
    ▼
JPEG/PNG страницы
    │
    ▼
src/image_ocr.py           ← Stage 2: OCR → сырой ответ
    │ --ocr-engine paddle|surya|external-qwen3
    │ --main-lang ru|en
    │ --fix-by-external-qwen3
    │
    ▼
.raw (сырой ответ VLM, неизменяемый)
    │
    ▼
src/generate_result.py     ← Stage 3: постпроцессинг
    │ фильтрация номеров страниц
    │ восстановление таблиц между страницами
    │ очистка формул (\text, \_)
    │
    ▼
.json + .md + объединённый .md + .tex + .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) 10
--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]  Отступ в пикселях    │
│                                                         (белая рамка /       │
│                                                         отступ при обрезке   │
│                                                         содержимого, по      │
│                                                         умолчанию 10)        │
│                                                         [default: 10]        │
│ --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
--main-lang Основной язык: ru (eslav rec) или en (PP-OCRv6 rec) ru
--fix-by-external-qwen3 Исправить ошибки VLM API (Qwen3.8 Max) OFF
--pause Пауза между изображениями (сек) 5

Выход: .raw-файл с сырым ответом движка (write-once, не изменяется при постпроцессинге).

Пример:

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

# Внешняя VLM как OCR-движок (Qwen3.8 Max) — лучший для русского
python -m src.image_ocr -i pages/ --ocr-engine external-qwen3

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

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

Stage 3: src/generate_result.py

Постпроцессинг .raw.json + .md + .tex + .html.

Параметр Описание По умолчанию
--input / -i Каталог с .raw-файлами required
--skip-latex Не генерировать LaTeX и HTML OFF

Что делает:

  1. Фильтрация номеров страниц — блоки вида 42, [73] в конце страницы удаляются
  2. Восстановление таблиц — таблицы, разорванные между страницами, объединяются (по кол-ву колонок)
  3. Очистка формул\text{score\_beh}score_beh для читаемости LLM
  4. Генерация — объединённый .md + .tex + .html (MathJax)
python -m src.generate_result -i cbr2/

--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        │
│    --main-lang                   <str>                 Основной язык: ru     │
│                                                        (eslav_PP-OCRv5_mobi… │
│                                                        или en                │
│                                                        (PP-OCRv6_medium_rec) │
│                                                        [default: ru]         │
│    --pause                       <int range>           Пауза между           │
│                                  [0<=x<=600]           изображениями в       │
│                                                        секундах (по          │
│                                                        умолчанию 5)          │
│                                                        [default: 5]          │
│    --help                                              Show this message and │
│                                                        exit.                 │
╰──────────────────────────────────────────────────────────────────────────────╯

Stage 3: src/generate_result.py--help

--help

 Usage: python -m src.generate_result [OPTIONS]

 Постобработка результатов OCR: фильтрация, таблицы, объединение, LaTeX+HTML.

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ *  --input       -i      <path>  Путь к каталогу с .raw-файлами             │
│                                  (результаты OCR)                           │
│                                  [required]                                 │
│    --skip-latex                  Не генерировать LaTeX и HTML               │
│    --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 — не коммитится.


Результат

Stage 2 (image_ocr.py) — сырой ответ

Файл Формат Описание
page_01.raw JSON Сырой ответ OCR-движка. Неизменяемый — пишется один раз, все постпроцессинги читают его

Stage 3 (generate_result.py) — итоговые файлы

Файл Формат Описание
page_01.json JSON Очищенный результат (фильтр номеров страниц, объединённые таблицы)
page_01.md Markdown Текст + $$-формулы + ##-заголовки. Готов к загрузке в LLM.
<dirname>.md Markdown Объединённый многостраничный документ
<dirname>.tex LaTeX LaTeX для всех страниц
<dirname>.html HTML HTML + MathJax для просмотра в браузере
# Полный цикл
python -m src.image_prepare -i scan.jpg -s 3:3 -o pages/
python -m src.image_ocr -i pages/ --ocr-engine external-qwen3
python -m src.generate_result -i pages/

Качество OCR

Движок Русский текст Английский Формулы Скорость
PaddleOCR --main-lang ru (eslav rec + v6 det) ~85-90% ~60% ✅ PP-FormulaNet ~100-160s CPU
PaddleOCR --main-lang en (v6 rec) ~75-80% ✅ PP-FormulaNet ~100-160s CPU
Surya 2 ~95% ~95% ✅ VLM ~300s CPU
External VLM (Qwen3.8 Max) ~98% ~98% ✅✅ ~60-120s API

Детектор: PP-OCRv6_medium_det (RepLKFPN) — +4.6% vs v5_server.


Передача в 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/
  models.py             — ParsedBlock, OcrPageResult
  image_utils.py        — calc_scale_dims
  env.py                — загрузка .env
  cli_utils.py          — run_main()
  image_prepare.py      — CLI Stage 1 (разрезание, unwarp, denoise)
  image_ocr.py          — CLI Stage 2 (OCR → .raw)
  generate_result.py    — CLI Stage 3 (постпроцессинг → .json/.md/.tex/.html)
  split/slicer.py        — разрезание, post-crop, border, unwarp, denoise
  ocr/
    paddle_engine.py     — PP-StructureV3 + v6-det + ru/en rec
    surya_engine.py      — Surya 2 + batch + CPU-оптимизации
    external_vlm_engine.py — Qwen3.8 Max full-page OCR
  postprocess/
    fixups.py            — OCR-ошибки (VaR, V^2)
    corrector.py         — apply_corrector (Callable)
    vlm_corrector.py     — Qwen3.8 Max per-block
    qwen_client.py       — общий Qwen API-клиент
  markdown/
    tables.py            — общие функции таблиц (parse, detect, ncols)
    generator.py         — Markdown + валидация LaTeX
  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.