На аккуратном скане OCR выглядит элементарно: подал файл, получил текст. С реальными документами схема рассыпается.

В одном PDF есть нормальный текстовый слой. В другом текст извлекается, но из-за сломанной карты ToUnicode превращается в кашу. Третий PDF целиком состоит из сканов, в четвёртом половина смысла спрятана в таблицах. Добавим повёрнутые страницы, фотографии документов, графики и сложную вёрстку.

Поэтому pro-ledin-ocr я собрал как маршрутизатор с постраничной проверкой качества. Он опубликован в PyPI и работает как CLI и Python-библиотека. Пока это бета-версия, не универсальный распознаватель любых документов.

входной файл
  → проверка типа и текстового слоя
  → прямое извлечение текста или OCR
  → отдельная обработка таблиц
  → проверка качества страниц
  → постраничная эскалация
  → Markdown, TXT или JSON

Почему здесь нужен маршрутизатор⌗

Выбор движка влияет на скорость, требования к железу, стоимость, приватность и структуру результата.

PDF с нормальным текстовым слоем незачем отправлять во внешний vision API: данные уйдут из локальной среды, а за обработку придётся платить. И наоборот, нет смысла мучить Tesseract страницей со сложной таблицей или схемой, если он уже вернул низкую уверенность.

В pro-ledin-ocr маршруты разделены на уровни:

Уровень Маршрут Для чего подходит
0 Текстовый слой + Camelot PDF с читаемым текстом и нативными таблицами
1 Tesseract Чистые сканы и печатный текст
2 EasyOCR Повреждённые сканы и часть рукописных материалов
2.5 PaddleOCR Многоязычные документы и повёрнутый текст
2.75 PaddleOCR-VL через MLX Таблицы и сложная структура документов на Apple Silicon
3 Vision Формы, графики, сложная вёрстка и визуально насыщенные страницы

Базовый движок задаётся явно. Приоритет настроек такой:

--engine → OCR_ENGINE → tesseract

Значения auto для движка нет. Автоматически выбирается маршрут документа, а не случайная модель.

ocr scan.pdf --engine tesseract --format md
ocr handwriting.jpg --engine easyocr --format md
ocr multilingual.pdf --engine paddleocr --format md

По команде видно, какой движок запустится и какие зависимости ему нужны. Для автоматических процессов это надёжнее магического выбора «лучшего» варианта.


Текстовый слой нужно проверять на читаемость⌗

Наличие текста в PDF не гарантирует, что его можно читать. Так бывает со сканами, поверх которых добавили невидимый текст, и со встроенными шрифтами без корректной карты ToUnicode: библиотека видит сотни символов и считает страницу текстовой, а на выходе получается каша.

pro-ledin-ocr проверяет два свойства слоя.

Сначала плотность: считает непробельные символы на каждой странице и берёт медиану. Затем читаемость наиболее плотных образцов:

  • долю букв и цифр;
  • долю фрагментов, похожих на слова, числа или единицы измерения;
  • долю управляющих символов и знаков замены U+FFFD.

Метаданные шрифтов тоже учитываются, но не принимают решение в одиночку. Отсутствие ToUnicode не всегда означает поломку: стандартные PDF-шрифты иногда остаются читаемыми и без этой карты.

Если слой плотный, но не похож на связный текст, инструмент отклоняет его и отправляет документ в OCR. После распознавания страница получает отметку text_layer_rejected в issues.

Решение, показатели читаемости и причина выбора доступны при проверке маршрута без распознавания:

ocr report.pdf --probe

Команда выводит NDJSON с полями needs_ocr, text_layer_rejected, readability и причиной выбора.

Сомнительную страницу лучше отправить на OCR, чем принять мусор за готовый текст и получить формально успешный, но испорченный результат.


Таблицы идут отдельной веткой⌗

Нативная таблица в PDF и таблица на скане требуют разных инструментов. Если у PDF читаемый текстовый слой, геометрия строк и столбцов уже есть в документе, поэтому Camelot запускается локально до OCR, отдельно от цепочки движков.

По умолчанию обработка таблиц включена:

ocr report.pdf --format md,json --out results/

В автоматическом режиме инструмент сначала получает кандидатов Camelot, а результаты режимов, отличных от stream, дополнительно сравнивает с вариантом stream. Алгоритм можно выбрать вручную:

# Таблицы с явными границами
ocr report.pdf --table-flavor lattice --format md,json --out results/

# Таблицы без границ, построенные на выравнивании текста
ocr report.pdf --table-flavor stream --format md,json --out results/

# Сетки, формы и повторяющиеся координаты
ocr report.pdf --table-flavor network --format md,json --out results/

# Смешанные признаки линий и выравнивания
ocr report.pdf --table-flavor hybrid --format md,json --out results/

Если таблицы мешают диагностике или нужен исходный линейный текст, их обработку можно отключить:

ocr report.pdf --no-tables --format md

Прямоугольника, похожего на таблицу, мало. Перед заменой текста инструмент проверяет кандидата:

  • достаточно ли строк и столбцов;
  • что показывает отчёт парсера Camelot;
  • сохранились ли текстовые фрагменты исходной области;
  • не потерялись ли числа;
  • совпадает ли область таблицы с блоками текстового слоя.

Плохой кандидат не заменяет исходный текст. Причина остаётся в JSON, например table_numeric_coverage_low или table_parse_quality_low.

Принятые таблицы встраиваются в Markdown вместо линейного текста той же области. Простые сетки превращаются в Markdown-таблицы, сложные — в HTML внутри Markdown. Если объединённые ячейки нельзя восстановить однозначно, структура упрощается, а результат получает пометку table_spans_flattened.


Сканы сначала обрабатывает Tesseract⌗

Если пригодного текстового слоя нет, инструмент растеризует страницы через доступный PDF-бэкенд и отправляет их в OCR. Поддерживаются Poppler и PyMuPDF; в базовой инструкции по установке указан Poppler.

Tesseract остаётся маршрутом по умолчанию:

ocr scan.pdf --format md
ocr scan.pdf --lang rus+eng --format md
ocr photo.jpg --engine tesseract --format md

При --lang auto Tesseract OSD определяет письменность по первой странице. Для кириллицы выбирается rus+eng, для латиницы — eng; другие письменности также сопоставляются с языковыми пакетами Tesseract и дополняются английским. Если страница короткая или шумная, язык лучше задать вручную. Для смешанного русского и английского я использую rus+eng.

Перед распознаванием можно выбрать обработку изображения:

Режим Что происходит
none Используется исходное изображение
basic Оттенки серого, контраст, резкость и увеличение маленьких изображений
enhanced Добавляются шумоподавление и адаптивная бинаризация
full Добавляется выравнивание наклонённой страницы
auto Режим выбирается по типу файла и доступным зависимостям

Например, для перекошенного скана:

ocr scan.pdf --preprocess full --lang rus+eng --format md,json --out results/

Усиленная обработка помогает не всегда. Адаптивная бинаризация может испортить чистый цифровой текст, поэтому уровень можно задать явно. Если OpenCV или NumPy недоступны, инструмент откатывается к basic, а при проблемах с Pillow берёт исходное изображение.


Контроль качества работает по страницам⌗

Документы редко бывают однородными: десять страниц распознаются нормально, а одиннадцатая оказывается фотографией таблицы. Качество нужно оценивать постранично.

Локальные OCR-движки возвращают текст, координаты слов и оценки уверенности. По ним pro-ledin-ocr отмечает страницы, которым не стоит доверять.

Сейчас страница попадает на проверку в двух случаях:

  • средняя уверенность ниже --min-conf;
  • расположение слов похоже на таблицу, но результат остался линейным.

Остальные диагностические отметки, включая text_layer_rejected, сохраняются в issues. Если страница эскалируется, они попадают и в историю решения.

Порог уверенности по умолчанию равен 60:

ocr scan.pdf --min-conf 70 --format md,json --out results/

Без настроенной эскалации CLI завершает обработку, перечисляет подозрительные страницы и предлагает повторить только их:

ocr scan.pdf --engine easyocr --pages 5,9 --format md

Или отправить выбранные страницы во внешний API:

ocr scan.pdf --engine vision --pages 5,9 \
  --vision-api-key "$KEY" \
  --vision-model my-vision-model \
  --format md

Большая часть документа остаётся на дешёвом локальном маршруте, а тяжёлый движок получает только страницы с конкретной проблемой.


Автоматическая эскалация⌗

Цепочку повторных попыток можно настроить заранее:

ocr scan.pdf \
  --engine tesseract \
  --auto-escalate easyocr,paddleocr \
  --format md,json \
  --out results/

В конце цепочки можно поставить внешний vision API:

ocr scan.pdf \
  --engine tesseract \
  --auto-escalate easyocr,vision \
  --vision-api-key "$KEY" \
  --vision-model my-vision-model \
  --format md,json \
  --out results/

Перед запуском инструмент проверяет зависимости базового движка и всей цепочки. Затем базовый движок обрабатывает документ, а на следующие уровни уходят только отмеченные страницы.

Каждая такая страница проходит указанные движки по порядку. Результаты локальных движков сравниваются по трём признакам:

  1. исчезли ли причины для проверки;
  2. какова средняя уверенность;
  3. сколько слов найдено.

Если цепочка доходит до vision или paddleocr-vl-mlx, их структурированный результат становится итоговым для страницы. Сопоставимой оценки mean_conf эти маршруты не дают.

Решение сохраняется в JSON:

{
  "decision": {
    "baseline": "tesseract",
    "flag_reason": [
      "low_confidence"
    ],
    "attempts": [
      {
        "engine": "tesseract",
        "status": "completed",
        "mean_conf": 42.1
      },
      {
        "engine": "easyocr",
        "status": "completed",
        "mean_conf": 78.4
      }
    ],
    "selected": "easyocr"
  }
}

Цепочка выполняется строго. Если нужного движка нет или одна из попыток падает, обработка останавливается. Инструмент не выкидывает проблемный этап и не подменяет его Tesseract. После такого сбоя результат в кэш не записывается.

Для PaddleOCR-VL на Apple Silicon используется полный клиент PaddleOCRVL. MLX обслуживает только VLM-этап:

ocr table.pdf \
  --engine paddleocr-vl-mlx \
  --pages 2,5 \
  --paddle-vl-server-url http://127.0.0.1:8111/ \
  --paddle-vl-model PaddlePaddle/PaddleOCR-VL-1.6 \
  --format md

MLX-сервер здесь не заменяет парсер документов. Сначала изображение проходит полный процесс PaddleOCR-VL, и только VLM-часть выполняется через локальный сервис.


Форматы результата⌗

Один запуск может создать три представления документа:

ocr document.pdf --format md,txt,json --out results/
results/
  document.md
  document.txt
  document.json

Markdown⌗

Markdown подходит для базы знаний, обработки AI-агентом и публикации. Принятые таблицы встраиваются в структуру страницы.

ocr document.pdf --format md --out document.md

TXT⌗

TXT сохраняет линейный текст. Для PDF с нативными таблицами реконструированная таблица не подменяет исходный фрагмент.

ocr document.pdf --format txt --out document.txt

JSON⌗

JSON нужен программе или агенту, если одного готового текста мало. Для каждой страницы сохраняются:

  • источник текста;
  • mean_conf;
  • issues и flag;
  • распознанные слова;
  • bbox каждого слова;
  • сведения о таблицах;
  • история эскалации и выбранный движок.

Для таблиц дополнительно записываются строки, область страницы, режим Camelot, показатели разбора, покрытие исходного текста, готовое представление и список проблем.

ocr document.pdf --format json --out document.json

Оценку уверенности возвращает не каждый движок. Vision-модель и PaddleOCR-VL выдают структурированный текст без показателя, сопоставимого с Tesseract. Поэтому mean_conf годится для проверки одного локального маршрута, но сравнивать по нему все движки нельзя.


Конфигурация через переменные среды⌗

Постоянные настройки можно вынести в переменные среды:

export OCR_ENGINE=tesseract
export OCR_AUTO_ESCALATE=easyocr,vision

export OCR_VISION_API_URL=http://localhost:8000/v1
export OCR_VISION_API_KEY="$KEY"
export OCR_VISION_MODEL=my-vision-model

После этого команда становится короче:

ocr scan.pdf --format md,json --out results/

Флаги CLI имеют приоритет над переменными среды. Эта команда запустит PaddleOCR независимо от OCR_ENGINE:

ocr scan.pdf --engine paddleocr --format md

Для vision действует тот же порядок:

--vision-api-url → OCR_VISION_API_URL
--vision-api-key → OCR_VISION_API_KEY
--vision-model → OCR_VISION_MODEL

Инструмент намеренно не читает универсальную переменную OPENAI_API_KEY. Документы не должны случайно уйти во внешний API только потому, что в окружении лежит ключ другого проекта.


Кэш, проверка зависимостей и атомарная запись⌗

В OCR-конвейере легко получить неполный результат: часть страниц готова, внешний API упал, а повреждённый JSON выглядит как законченный отчёт. От этого защищают кэш, предварительная проверка зависимостей и атомарная запись.

Кэширование⌗

Кэш включается явно:

ocr FILE1.pdf FILE2.pdf \
  --cache ocr-cache.json \
  --format txt \
  --out results/

Ключ учитывает путь к файлу, время изменения, размер, движок, язык, DPI, обработку изображения, диапазон и лимит страниц, PSM, порог уверенности, очистку текста, цепочку эскалации и параметры таблиц. Для vision в ключ также входят адрес API, модель и промпт; для PaddleOCR-VL — адрес MLX-сервера и модель.

Версия схемы результата тоже входит в ключ, поэтому несовместимые старые записи не используются. Принудительный повторный запуск выполняется через --force:

ocr document.pdf --cache ocr-cache.json --force --format md

Предварительная проверка зависимостей⌗

До распознавания инструмент проверяет исполняемые файлы, Python-пакеты и обязательные настройки всей выбранной цепочки.

Для программной интеграции есть отдельный API:

from pro.ledin import ocr

requirement = ocr.probe_engine_requirements(
    "vision",
    vision_api_key="key",
    vision_model="model",
)

if not requirement.available:
    print(requirement.to_dict())

Проверка не импортирует тяжёлые OCR-пакеты, не загружает модели, не обращается к сети и не читает универсальные переменные с ключами.

В результате есть стабильный код ошибки, список отсутствующих компонентов, тип зависимости и название нужного extra. Оркестратор отличит отсутствующий Tesseract от ненастроенной vision-модели или отсутствующего PaddlePaddle.

Для PDF есть отдельная проверка:

requirement = ocr.probe_pdf_requirements()

Она проверяет бэкенды растеризации и извлечения текста. Достаточно доступности одного из поддерживаемых вариантов: Poppler или PyMuPDF.


Установка CLI через uv tool⌗

Базовая версия устанавливается так:

uv tool install pro-ledin-ocr

В неё входят Camelot и клиент для OpenAI-совместимого vision API.

Дополнительные Python-движки и инструменты ставятся через extra:

uv tool install "pro-ledin-ocr[all]"

Первый запуск EasyOCR и PaddleOCR может загрузить модели. В изолированной или полностью локальной среде их нужно подготовить заранее.

Для базового маршрута также нужны системные зависимости.

macOS⌗

brew install poppler tesseract tesseract-lang

Проверка:

pdftoppm -v
pdfinfo -v
tesseract --version
tesseract --list-langs

Debian и Ubuntu⌗

Для русского и английского:

sudo apt update
sudo apt install poppler-utils tesseract-ocr tesseract-ocr-rus

Или пакет со всеми доступными языками:

sudo apt install poppler-utils tesseract-ocr-all

Проверка та же:

pdftoppm -v
pdfinfo -v
tesseract --version
tesseract --list-langs

Скилл для AI-агентов⌗

В репозитории лежит отдельный OCR-скилл:

npx skills add ledin-pro/ocr

CLI отвечает на вопрос «как распознать файл». Скилл нужен, когда:

  1. Модель агента не поддерживает извлечение текста из изображений.
  2. Нужно массово распознать сотни или тысячи страниц, а обработка через платные API обойдётся слишком дорого.
  3. Конфиденциальные данные нельзя отправлять за пределы своего контура.

В этих сценариях скилл объясняет агенту, как выбрать маршрут:

  • когда достаточно текстового слоя;
  • почему таблицу в текстовом PDF сначала нужно отдать Camelot;
  • когда запускать Tesseract;
  • какие страницы повторять через EasyOCR или PaddleOCR;
  • когда можно подключать vision;
  • как проверить зависимости без загрузки моделей;
  • почему нельзя молча убрать отсутствующий движок из заданной цепочки;
  • как читать mean_conf, issues, bbox и решения об эскалации;
  • когда нужно остановиться и запросить разрешение на установку тяжёлой зависимости или передачу документа во внешний API.

Без этих правил агент легко выбирает один и тот же инструмент для любого файла. Здесь порядок другой: определить структуру документа, проверить качество и усилить обработку только на проблемных страницах.


Ограничения⌗

У бета-версии есть ограничения.

Зашифрованные PDF⌗

Инструмент определяет шифрование и останавливается. Перед обработкой файл нужно расшифровать; OCR не пытается обходить защиту.

Таблицы с продолжением на следующей странице⌗

Camelot работает постранично. Таблица, начатая на одной странице и продолженная на другой, останется двумя фрагментами. Автоматическое объединение рискованно: нужно учитывать повтор заголовка, разрывы строк и изменение сетки.

Сложные сканы и графики⌗

Сильный наклон, тени, рукописные пометки, печати, несколько колонок и низкое разрешение всё ещё ломают локальное распознавание.

С графиками ещё сложнее. Vision-модель может прочитать подписи осей и часть значений, но не гарантирует точного восстановления исходных данных.

Приватность внешнего vision API⌗

Режим vision отправляет отрисованные страницы во внешний OpenAI-совместимый API. Перед запуском нужно проверить правила хранения и обработки документов у выбранного поставщика.

Для чувствительных данных лучше оставить локальные движки или поднять совместимый сервер у себя.

Нет общей метрики точности⌗

Tesseract, EasyOCR, PaddleOCR, Camelot и vision по-разному оценивают результат. Число 85 у одного движка нельзя напрямую сравнивать с 85 у другого.

Для своего набора документов нужен корпус с эталонным текстом. Я бы отдельно измерял обычный текст, числа, таблицы, порядок чтения и сохранение структуры. Одна сводная цифра легко прячет ошибку, которая окажется критичной в прикладном сценарии.