OCR-конвейер с эскалацией: CLI + Skill
Содержание
На аккуратном скане 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/
Перед запуском инструмент проверяет зависимости базового движка и всей цепочки. Затем базовый движок обрабатывает документ, а на следующие уровни уходят только отмеченные страницы.
Каждая такая страница проходит указанные движки по порядку. Результаты локальных движков сравниваются по трём признакам:
- исчезли ли причины для проверки;
- какова средняя уверенность;
- сколько слов найдено.
Если цепочка доходит до 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 отвечает на вопрос «как распознать файл». Скилл нужен, когда:
- Модель агента не поддерживает извлечение текста из изображений.
- Нужно массово распознать сотни или тысячи страниц, а обработка через платные API обойдётся слишком дорого.
- Конфиденциальные данные нельзя отправлять за пределы своего контура.
В этих сценариях скилл объясняет агенту, как выбрать маршрут:
- когда достаточно текстового слоя;
- почему таблицу в текстовом 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 у другого.
Для своего набора документов нужен корпус с эталонным текстом. Я бы отдельно измерял обычный текст, числа, таблицы, порядок чтения и сохранение структуры. Одна сводная цифра легко прячет ошибку, которая окажется критичной в прикладном сценарии.