# 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_to_latex.py ← Stage 2: OCR + Markdown │ --ocr-engine paddle|surya │ --input file.jpg (одна страница) │ --input pages/ (пакетный режим: все изображения в каталоге) │ --llm, --vlm (optional) │ ├─ 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` | Сетка `:` | — | | `--slice-auto` / `-a` | Автоопределение сетки | — | | `--output-dir` / `-o` | Каталог для страниц | `.` | | `--pre-rotate` / `-r` | Поворот: 90, 180, 270 | — | | `--border` / `-b` | Белая рамка (px) | 50 | | `--post-crop` / `--no-post-crop` | Обрезка по контенту | ON | Пример: ```bash 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_to_latex.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 | Пример: ```bash # Одна страница python -m src.image_to_latex -i page.jpg # Пакетный режим — все изображения в каталоге (Surya: batch-оптимизация) python -m src.image_to_latex -i ./pages/ --ocr-engine surya # Пакетный режим (PaddleOCR: последовательно) python -m src.image_to_latex -i ./pages/ --ocr-engine paddle ``` ### Переменные окружения (`.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) | `.env` уже добавлен в `.gitignore` — не коммитится. --- ## Результат CLI (`image_to_latex.py`) генерирует два файла для каждого изображения: | Файл | Формат | Описание | |------|--------|----------| | `page_01.md` | Markdown | Текст + `$$`-формулы + `##`-заголовки. **Готов к загрузке в LLM.** | | `page_01.json` | JSON | Полный дамп OCR: bbox, confidence, labels, formulas | **Другие форматы — ручные утилиты:** | Файл | Утилита | Назначение | |------|---------|------------| | `.html` | `src/latex/surya2html.py` | Surya JSON → HTML с MathJax (формулы рендерятся браузером) | | `.html` | `src/latex/tex2html.py` | LaTeX `.tex` → HTML + MathJax | ```bash # Surya JSON → HTML python -c "from src.latex.surya2html import surya_json_to_html; \ open('page.html','w').write(surya_json_to_html('page.json'))" # TeX → HTML (если есть .tex файл) python src/latex/tex2html.py page.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_split.py — CLI Stage 1 image_to_latex.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/ surya2html.py — Surya JSON → HTML tex2html.py — TeX → HTML ``` --- ## Не входит в проект - GUI - Docker - PDF (на вход) - LLM/RAG - Поиск по документам - Многопоточность - Автоматический deskew / denoise --- ## Лицензия MIT. Модели PaddleOCR — Apache 2.0, Surya 2 — OpenRAIL-M.