# Stage 2 Results — OCR Pipeline
## Путь проекта
### Исходная точка
Единый скрипт `src/cli.py` → разрезание изображений на страницы (Stage 1). Нужен Stage 2 — распознавание текста и структуры документа, генерация LaTeX/HTML/Markdown для последующего анализа LLM.
---
## Опробованные варианты
### 1. PaddleOCR базовый (PP-OCRv5)
**Результат:** 66 текстовых блоков, русский текст — ~60% точности.
- `i-WHAeKC AOrOBOpa` вместо `і - индекс договора`
- `t - номер месяцаB дефолте` — латиница смешана с кириллицей
- Нет структуры документа (заголовки, формулы, таблицы слиты в сплошной текст)
- **Вывод:** только для английского, для русского требуется структура
### 2. PP-StructureV3 (ансамбль специализированных моделей)
**Результат:** 22 структурированных блока с layout-разметкой.
- Layout detection: ✅ `[number]`, `[formula]`, `[text]`, `[paragraph_title]`
- Display-формулы: ✅ `\mathrm{C Rec}_{\mathrm{it}}=\sum...`
- Таблицы: ✅ PP-TableMagic с bbox ячеек
- Русский текст: ⚠️ ~85% (eslav_PP-OCRv5_mobile_rec)
- **Вывод:** структура отлично, текст — терпимо
### 3. Замена на cyrillic_PP-OCRv5_mobile_rec
**Результат:** разница ~1-2%, несущественна.
- eslav: `LGD_ехtг;` (кириллица г)
- cyrillic: `LGD_ехtr;` (латиница r) — чуть лучше
- **Вывод:** обе мобильные модели, серверной русской не существует
### 4. PP-OCRv5_server_rec / PP-OCRv6
**Результат:** ❌ не поддерживают русский.
- Server model: только CN/JP/EN
- PP-OCRv6: 46 латинских языков + CN/JP/EN (без кириллицы)
- **Вывод:** русский — только mobile-версии
### 5. PP-OCRv6 детектор (RepLKFPN, 62MB) ✅ оставлен
**Результат:** +4.6% detection accuracy vs v5, на 7% быстрее (97.8s vs 104.7s).
- Модель скачана через paddlex[ocr] 3.7.2 без апгрейда paddlepaddle
- Смешан с eslav_rec — детектор язык-независим
- Совместим с PP-StructureV3 через YAML-конфиг
- **Вывод:** бесплатный прирост детекции без риска
### 6. `--main-lang` выбор распознавалки ✅ оставлен
**Результат:** переключение rec-модели по основному языку страницы.
- `--main-lang=ru` → `eslav_PP-OCRv5_mobile_rec` (русский, 85-90%)
- `--main-lang=en` → `PP-OCRv6_medium_rec` (английский + греческие в формулах, 75-80%)
- Таблицы и формулы — общие (SLANet + PP-FormulaNet)
- Модели кешируются раздельно (lru_cache по языку)
- **Вывод:** +15-20% на английских страницах заменой одной модели
### 7. Локальный LLM-корректор (Qwen2.5-7B Q4_K_M) ❌ удалён
**Результат:** 50/50 — исправляет и тут же галлюцинирует.
```
До: сypporat WoE для гpynnы i
После: коррелят weight of evidence (галлюцинация вместо «суррогат»)
```
- Причины отказа: (1) слепой — не видит изображение, догадывается; (2) маленький — 7B квантизованный не понимает контекст; (3) избыточный — `fixups.py` уже правит VaR/V^2 без модели
- Занятые ресурсы: 4.4 GB диск + 5 GB RAM + 60s/блок
- **Вывод:** удалён, освобождено 4.4 GB
### 8. VLM-корректор (Qwen3.8 Max через OpenCode API)
**Результат:** полное восстановление текста + авто inline-формулы.
```
До: ΣДсе, - дисконтированная сумма возвратов
После: $\sum_t DCF_{i,t}$ — дисконтированная сумма возвратов
```
- ~10s/блок, Anthropic API `zen/go/v1/messages`
- Таблицы: HTML → tab-separated flat text ✅
- На порядок качественнее LLM — видит изображение, читает напрямую
- **Вывод:** лучшее качество, требует API-доступа
### 9. Docling (IBM Research)
**Результат:** ❌ не поддерживает PaddleOCR как OCR-backend.
- Поддерживает: EasyOCR, Tesseract, RapidOCR, Nemotron
- **Вывод:** несовместим с нашим пайплайном
### 10. PaddleOCR-VL (ERNIE-4.5-0.3B)
**Результат:** модель загружена (1.79 GB), ~15 мин/стр на CPU.
- Одна VLM вместо ансамбля
- Теоретически решает проблему русского (LM понимает контекст)
- **Вывод:** правильный подход при GPU, нереализуем на CPU
### 11. Surya 2 (Datalab, 650M VLM) ⭐
**Результат:** лучший русский OCR из коробки — но медленно на CPU.
- 95%+ точность русского текста без коррекции
- Авто inline-формулы: `` → `$MRec_{it}$`
- ~300s/стр после CPU-оптимизаций (vs 140s PaddleOCR)
- **Слабость:** таблицы требуют много токенов → таймауты на CPU
- **Вывод:** лучший выбор по качеству, медленнее на CPU
### 12. VLM full-page (Qwen3.8 Max) для таблиц
**Результат:** 118s на страницу с таблицей, качество отличное.
- Таблица → pipe-Markdown: `| col1 | col2 |`
- Формулы → `$...$`
- Структура заголовков сохранена
- **Вывод:** для табличных страниц — лучший вариант
---
## Эксперименты по препроцессингу изображений
### 13. Denoising (Non-Local Means) ✅ оставлен
**Результат:** 17 блоков vs 12 без (+42%), исправил «6»→«G», «Пр»→«р».
- Без denoise: «рогнозное», «L6D ID», 12 блоков, 104.7s
- С denoise: «Прогнозное», «LGD ID», 17 блоков, 139.5s (+33% времени)
- **Вывод:** +42% блоков за +35s — оставлен как опция `--denoise`
### 14. Адаптивная бинаризация (Otsu/Adaptive) ❌ удалена
**Результат:** жёсткие границы порвали буквы, ложные контуры.
- «рассчитывается» → «рассчита**встся**»
- Формулы: `$b_i$` → `$b_{-}i-\\sec a$` (мусор)
- Мусорные блоки: `$\alpha\times\alpha\times\alpha...$` — артефакты бинаризации
- **Причина:** нейросетевой OCR обучен на естественных изображениях, не на бинарных
- **Вывод:** удалена из CLI и пайплайна
### 15. Upscale ×2 для мелкого текста ❌ удалён
**Результат:** бесполезен — PaddleOCR режет `max_side_limit=4000`.
- Изображение 6090×3880 (×2) → Paddle сжимает обратно до ≤4000
- Время впустую: 174s вместо 104s
- **Причина:** параметр `max_side_limit` жёстко ограничивает входное разрешение
- **Вывод:** удалён из CLI, требует правки `max_side_limit` в YAML для пользы
### 16. Document Unwarping (Canny → 4-угольник → perspective) ✅ оставлен
**Результат:** работает для фото под углом, NOP для плоского сканера.
- Планшетный сканер: контур страницы не найден → возврат исходника
- Фото: детектит 4-угольник → warp в прямоугольник
- Баг первой версии: детектил внутренние таблицы (bbox ~150px высотой)
- **Исправление:** порог `min_area ≥ 30%` от изображения
- **Вывод:** опция `--unwarp` для фото, для сканера — YAGNI
---
## Оптимизации кода (SOLID / DRY / KISS / YAGNI)
| Действие | Результат |
|---|---|
| `ParsedBlock`/`OcrPageResult` → `src/models.py` | 7 модулей отвязаны от paddle_engine |
| `Corrector(Protocol)` → `Callable` | Удалён неиспользуемый Protocol |
| `validate_output_dir()` удалён | Мёртвый код из `slicer.py` |
| `_ensure_env()` → `load_env()` напрямую | Убраны дублирующие guard'ы |
| `calc_scale_dims()` в `image_utils.py` | Общий pre-scale для Surya + external VLM |
| `functools.lru_cache` для engine-синглтонов | Убран boilerplate `_engine: X \| None` |
| `surya2html.py`, `tex2html.py` удалены | Дубликаты логики из `json_to_latex.py` |
| `binarize_image()`, `upscale_image()` удалены | Мёртвые функции из `slicer.py` |
| `requirements.txt`: 118 → 14 пакетов | Только прямые зависимости |
| `llm_corrector.py` + Qwen2.5-7B (4.4GB) удалены | Галлюцинации, YAGNI |
---
## Итоговая архитектура
```
Входное изображение (JPEG/PNG/TIFF)
│
▼
src/image_prepare.py ← Stage 1: подготовка
│ --slice / --slice-auto
│ --pre-rotate, --border
│ --unwarp, --denoise
│ --post-crop
│
▼
src/image_ocr.py ← Stage 2: OCR + Markdown
│ --ocr-engine paddle|surya|external-qwen3
│ --main-lang ru|en
│ --fix-by-external-qwen3
│ --pause, --no-result-one-document
│
├─ PaddleEngine / SuryaEngine (OCR)
├─ fix_ocr_errors (VaR, V^2)
├─ VlmCorrector (Qwen3.8 Max, API)
│
▼
.md + .json (per page) + merged.md
│
▼
src/latex/json_to_latex.py ← Stage 3: JSON → LaTeX + HTML
│ Paddle / Surya / Qwen
│ Автоопределение формата
│
▼
.tex + .html (с MathJax для таблиц и формул)
```
### По умолчанию включено
| Функция | Статус |
|---------|:------:|
| `--main-lang ru` | ON |
| Resume (пропуск JSON) | ON |
| `--result-one-document` | ON |
| `--pause 5` | ON |
| `--post-crop` (Stage 1) | ON |
### Модули
```
src/
models.py — ParsedBlock, OcrPageResult
image_utils.py — calc_scale_dims
image_prepare.py — CLI Stage 1
image_ocr.py — CLI Stage 2
cli_utils.py — run_main()
env.py — загрузка .env
split/slicer.py — разрезание, post-crop, border, unwarp, denoise
ocr/
paddle_engine.py — PP-StructureV3 + v6-det + ru/en rec
surya_engine.py — Surya 2 + CPU-оптимизации + health_check
external_vlm_engine.py — Qwen3.8 Max full-page OCR
postprocess/
fixups.py — VaR, V^2
corrector.py — apply_corrector
vlm_corrector.py — Qwen3.8 Max per-block API
qwen_client.py — общий Qwen API-клиент
markdown/generator.py — Markdown + валидация LaTeX
latex/
json_to_latex.py — JSON→LaTeX+HTML (Paddle + Surya + Qwen)
```
---
## Оптимизации Surya 2 для CPU
| Параметр | До | После | Эффект |
|----------|:---:|:---:|--------|
| KEEP_ALIVE | off | on | -23s повторный старт |
| CTX per slot | 12288 | 8192 | -30% RAM |
| Parallel slots | 8 | 1 | -80% слотов |
| Pre-scale | 2552px | 1056px | -60% пикселей |
| Threads | default | `-t 8` | стабильно |
| **Итого** | **430s** | **300s** | **-30%** |
---
## Сравнение движков
| Параметр | PaddleOCR | Surya 2 | VLM Qwen3.8 |
|----------|:---:|:---:|:---:|
| Архитектура | Ансамбль 5 моделей | Один VLM (650M) | Cloud VLM |
| Русский текст | ~85% (eslav rec) | 95%+ | 98%+ |
| Английский текст | ~80% (v6 rec) | 95%+ | 98%+ |
| Формулы | PP-FormulaNet | VLM (отлично) | VLM (отлично) |
| Таблицы | PP-TableMagic + bbox | ❌ CPU timeout | ✅ Pipe-Markdown |
| Inline-формулы | Нет | `