# 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) ├─ LLM corrector (Qwen2.5-7B, local) ├─ VLM corrector (Qwen3.8 Max, API) │ ▼ .md + .json + .html ``` --- ## Параметры ### Stage 1: `src/image_prepare.py` | Параметр | Описание | По умолчанию | |----------|----------|:---:| | `--input` / `-i` | Путь к входному изображению | required | | `--slice` / `-s` | Сетка `:` | — | | `--slice-auto` / `-a` | Автоопределение сетки | — | | `--output-dir` / `-o` | Каталог для страниц | `.` | | `--pre-rotate` / `-r` | Поворот: 90, 180, 270 | — | | `--border` / `-b` | Белая рамка (px) | 50 | | `--unwarp` / `-u` | Выпрямить перспективные искажения (Canny → 4-угольный контур → warp) | OFF | | `--post-crop` / `--no-post-crop` | Обрезка по контенту | ON | Пример: ```bash 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 ```
--help ``` Usage: python -m src.image_prepare [OPTIONS] Подготовить отсканированное изображение: разрезать по сетке, обрезать, добавить рамку. ╭─ Options ────────────────────────────────────────────────────────────────────╮ │ --input -i Путь к входному │ │ изображению (JPEG, │ │ PNG, TIFF) │ │ --slice -s Размер сетки в │ │ формате │ │ <колонки>:<строки>, │ │ например 3:2 │ │ --slice-auto -a Автоматически │ │ определить размер │ │ сетки по содержимому │ │ изображения │ │ --output-dir -o Каталог для │ │ сохранения │ │ результатов (по │ │ умолчанию — текущий) │ │ [default: .] │ │ --pre-rotate -r Поворот изображения │ │ перед разрезанием: │ │ 90, 180 или 270 │ │ --border -b [x>=0] Отступ в пикселях │ │ (белая рамка / │ │ отступ при обрезке │ │ содержимого, по │ │ умолчанию 50) │ │ [default: 50] │ │ --unwarp -u Выпрямить │ │ перспективные │ │ искажения перед │ │ разрезанием │ │ --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-llm` | Исправить ошибки локальным LLM (Qwen2.5-7B) | OFF | | `--fix-by-external-qwen3` | Исправить ошибки VLM API (Qwen3.8 Max) | OFF | | `--pause` | Пауза между изображениями (сек) | 5 | | `--no-result-one-document` | Не объединять `.md` в один документ | — | | `--no-post-crop` | Не обрезать по контенту (Stage 1) | — | Пример: ```bash # Одна страница — 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 # С LLM-корректором python -m src.image_ocr -i page.jpg --ocr-engine paddle --fix-by-llm ```
--help ``` Usage: python -m src.image_ocr [OPTIONS] OCR → PostProcess → Markdown. Принимает файл или каталог изображений. ╭─ Options ────────────────────────────────────────────────────────────────────╮ │ * --input -i Путь к входному │ │ изображению или │ │ каталогу (JPEG, PNG, │ │ TIFF) │ │ [required] │ │ * --ocr-engine OCR-движок: paddle │ │ (PP-StructureV3), │ │ surya (Surya 2 VLM) │ │ или external-qwen3 │ │ (Qwen3.8 Max API) │ │ [required] │ │ --output-dir -o Каталог для │ │ сохранения │ │ результатов (по │ │ умолчанию — рядом с │ │ входным файлом) │ │ --fix-by-llm Исправить OCR-ошибки │ │ локальным LLM │ │ (Qwen2.5-7B). Не │ │ работает с │ │ --ocr-engine │ │ external-qwen3 │ │ --fix-by-external-q… Исправить OCR-ошибки │ │ внешней VLM (Qwen3.8 │ │ Max, API). Не │ │ работает с │ │ --ocr-engine │ │ external-qwen3 │ │ --no-result-one-doc… Не объединять .md │ │ результаты в один │ │ документ │ │ [default: True] │ │ --pause Пауза между │ │ [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`) Скопируй шаблон и заполни своими ключами: ```bash cp .env.dist .env # Отредактируй .env — вставь свои API-ключи ``` | Переменная | Назначение | Где взять | |-----------|-----------|-----------| | `OPENCODE_API_KEY` | VLM-корректор через OpenCode Go | [opencode.ai/auth](https://opencode.ai/auth) → скопировать ключ | | `HF_TOKEN` | Скачивание моделей с HuggingFace (Surya 2) | [huggingface.co/settings/tokens](https://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 | **При пакетной обработке** (каталог на входе) — дополнительно: | Файл | Описание | |------|----------| | `.md` | Объединённый многостраничный Markdown (по умолчанию) | | `.tex` | LaTeX для всех страниц (через `json_to_latex.py`) | **Ручные утилиты:** | Файл | Утилита | Назначение | |------|---------|------------| | `.tex` | `src/latex/json_to_latex.py` | JSON (Paddle / Surya / Qwen) → LaTeX + HTML (автодетект) | ```bash # 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 | | + 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.jpg` — `max_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 llm_corrector.py — Qwen2.5-7B (local) 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 / denoise --- ## Лицензия MIT. Модели PaddleOCR — Apache 2.0, Surya 2 — OpenRAIL-M.