# 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}` → `$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-формулы | Нет | `` тэги | `$...$` | | Скорость CPU | ~100-160s | ~300s | ~120s (API) | | Офлайн | ✅ | ✅ | ❌ | | Размер моделей | ~2.5 GB | ~1.5 GB | 0 (cloud) | --- ## Ключевые цифры (на CPU: 8 ядер, 32 GB RAM) | Этап | PaddleOCR | Surya 2 | VLM | |------|:---:|:---:|:---:| | OCR (одна страница) | 100-160s | 300s | 120s | | Denoise (Stage 1) | +35s | — | — | | VLM-корректор | +10s/блок | не нужен | — | | JSON + MD сохранение | <1s | <1s | <1s | | Merge в один документ | <1s | <1s | <1s | | **Итого на страницу** | ~3-4 min | ~5 min | ~2 min | | **12 страниц (batch)** | ~15-20 min* | ~60 min | ~24 min | \* PaddleOCR с `--pause 5` и `--denoise` --- ## Неиспользованные подходы 1. **EasyOCR** — WER 0.12 vs Paddle 0.056 2. **Tesseract** — нет GPU, хуже на русском 3. **RapidOCR** — легче, качество ниже 4. **Fine-tune eslav** — требует GPU + датасет --- ## Выводы 1. **PP-StructureV3 + v6-det + eslav_rec** — лучший офлайн OCR для русского на CPU: 85-90% текст, 90% формулы, 80% таблицы 2. **PP-OCRv6 rec** через `--main-lang=en` — +20% на английских страницах 3. **Denoise** — +42% блоков на сканах с шумом, оставлен как опция 4. **Бинаризация, upscale** — вредны для нейросетевого OCR, удалены 5. **Локальный LLM (Qwen2.5-7B)** — галлюцинирует, удалён 6. **VLM-корректор (Qwen3.8 Max)** — отличный, но API 7. **Surya 2** — лучший русский OCR из коробки, CPU-оптимизации дали -30% времени 8. **Гибридный подход**: Surya для текста + VLM для таблиц = лучший баланс 9. **CPU-ограничения** — основной фактор, GPU сделал бы Surya/PaddleOCR-VL идеальным выбором