# 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` | Сетка `:` | — |
| `--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_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 |
| `--result-one-document` | Объединить `.md` в один документ | ON |
| `--no-result-one-document` | Не объединять | — |
Пример:
```bash
# Одна страница
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
```
### 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 |
**Другие форматы — ручные утилиты:**
| Файл | Утилита | Назначение |
|------|---------|------------|
| `.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_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.