Возьмём одну строку из акта приёмки, отсканированного в конторе, где принтер помнит ещё прошлую бухгалтерию: «Деталь 40О7-12 принята 03.05.2024, дефектов не обнаружено». Человек читает её за секунду. OCR может выдать «4007-12», «40О7-12» с русской «О» или «40O7-12» с латинской. Дата превратится в «03.05.2О24» или «03,05.2024», потому что точка на бледном сканере почти исчезла. А «не обнаружено» легко потеряет «не», если частица стояла у самого края строки и попала под тень от переплёта.

Для языковой модели все эти варианты выглядят как обычный текст. Она не видит, что цифра ноль и буква «О» отличаются в исходнике на пару пикселей. Если пользователь спросит «где принята деталь 4007-12», индекс по строке «40О7-12» её не найдёт: токены разные, эмбеддинг в лучшем случае окажется поблизости, в худшем уведёт к соседней детали. А ответ «дефекты обнаружены» вместо «не обнаружены» опаснее молчания: он читается гладко и ошибочен по смыслу.

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

Сначала сортируем документы по состоянию изображения

Расширение .pdf почти ничего не говорит о содержимом. Внутри может лежать аккуратный текстовый слой, фотография страницы без единого символа или смесь того и другого. От этого зависит, нужен ли OCR вообще, и если нужен, то на всей ли странице.

Практичная сортировка делается по устройству файла и по качеству картинки:

  • PDF с корректным текстовым слоем. Выделяется мышью, копируется осмысленно. Здесь OCR чаще вредит, чем помогает: вы заменяете точный текст приблизительным. Достаточно проверить, что слой не мусорный (кириллица не превратилась в «Ñïðàâêà» из-за неверной кодировки).
  • Скан без слоя. Страница целиком растровая. Весь текст придётся получать распознаванием.
  • Смешанная страница. Типичный случай: печатный бланк с родным текстовым слоем, поверх которого вставлена отсканированная печать, рукописная пометка или фотография таблички. Слой есть, но он описывает только часть того, что видит человек.
  • Фото вместо скана. Телефонный снимок листа на столе: перекос, неравномерное освещение, загнутые углы. Формально это тоже изображение в PDF, но проблемы у него другие, чем у плоского сканера.

Сортировать стоит не вручную по всей коллекции, а по небольшой выборке из каждой группы: откройте по десять-пятнадцать файлов, попробуйте выделить текст, посмотрите на разрешение и на то, насколько ровно лежит страница. Двадцати минут хватает, чтобы понять, из чего на самом деле состоит архив. Часто выясняется, что «сканы» — это на две трети нормальные PDF из 1С и лишь остальное требует настоящей работы.

Масштаб, перекос, фон и шум — четыре разные поломки

Когда распознавание ошибается, хочется списать всё на «плохое качество». Но у ошибок разные причины, и лечатся они по-разному. В документации Tesseract по улучшению качества среди факторов называются масштаб, фон, шум, перекос и сегментация страницы. Полезно относиться к ним как к отдельным диагнозам.

Масштаб

Если буквы на изображении слишком мелкие, движок не различает детали, которыми кириллица богата: «и» и «й», «ь» и «ъ», «з» и «э». Именно поэтому в русских текстах мелкий шрифт ломается заметнее, чем в английских. Признак проблемы: ошибки равномерно рассыпаны по всей странице, особенно в сносках, в мелком петите договоров, в реквизитах. Лечится пересъёмкой с большим разрешением или аккуратным увеличением перед распознаванием. Но увеличение маленькой картинки не добавляет информации: размытое остаётся размытым, только крупнее.

Перекос

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

Фон

Серый или жёлтый фон, штамп поверх текста, водяной знак «КОПИЯ», просвечивающая обратная сторона листа. Движку нужно отделить буквы от фона, а с неровным фоном он принимает часть пятен за знаки или, наоборот, теряет бледные символы. Признак: странные вставки из точек и запятых в пустых местах и пропавшие слова там, где под текстом проходит печать. Помогает бинаризация (перевод в чёрно-белое с адаптивным порогом), но тот же приём способен уничтожить тонкий шрифт.

Шум

Крапинки от плохой бумаги, следы тонера, артефакты сжатия JPEG. Шум порождает именно те подмены, с которых мы начали: пылинка рядом с «0» превращает его в «О» или «6», а лишняя точка над запятой делает её точкой с запятой. Признак: правдоподобные, но неверные символы в цифрах и знаках препинания. Это самый опасный класс, потому что результат выглядит осмысленно.

Важная оговорка: любая предобработка — это ставка, а не гарантия. Улучшенное изображение не обязано дать лучший текст, потому что фильтр, убравший шум, заодно уносит тонкие штрихи букв. Поэтому оригинал нужно хранить нетронутым, а обработанную копию считать промежуточным артефактом, который можно переделать. Сравнивайте результат на ваших страницах, а не доверяйте общему ощущению, что чёткая картинка всегда лучше.

Гибридное или полностраничное распознавание: решает устройство PDF

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

Какой режим подходит к какому PDF
Устройство PDFЧто разумно делатьЧем рискуете
Корректный текстовый слой, картинок почти нетБрать слой как есть, OCR не запускатьСломанная кодировка слоя; проверяйте выборкой
Скан без слояРаспознавать всю страницу целикомВремя и зависимости движка
Смешанная страница: слой плюс вставленные изображенияГибридный режим: слой там, где он есть, OCR только для растровых областейСлой может быть неполным или не совпадать с картинкой
Слой есть, но подозрительный (мусор, неверный порядок)Полностраничный OCR и сравнение со слоем на выборкеПотеря точного текста там, где слой был хорош

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

Смешанная страница — самый коварный случай. Представьте бланк накладной, где печатные поля идут родным слоем, а подпись и круглая печать вставлены картинкой, причём на печати написан номер доверенности. Слой даст вам бланк без номера. Пользователь спросит про доверенность, и RAG честно ответит, что такой информации в документе нет. Это не ошибка распознавания в строгом смысле: распознавать было нечего, потому что движок не заглянул в картинку. Гибридный режим как раз закрывает такие дыры, а контрольные вопросы с «картиночными» фактами помогают заметить, что дыра есть.

Для запуска такой обработки на своём сервере нужен процессор с запасом по ядрам и достаточно оперативной памяти: страницы в высоком разрешении занимают в памяти заметно больше, чем их размер на диске. Для пробной партии подойдёт и небольшой VPS, например тариф Uab (2 vCPU, 4 ГБ) или Heka (4 vCPU, 8 ГБ) у MAATRIX, а для регулярной обработки больших архивов смотрят на ступени выше. Эти ресурсы не обещают конкретной скорости: реальную пропускную способность в страницах в минуту лучше замерить на своей выборке, а GPU в публичном предложении не указан, поэтому планируйте работу на CPU.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Текст без адреса — это текст без доказательств

Допустим, распознавание сработало, и вы получили чистый поток текста. Если вы сложите его в индекс как есть, то потеряете главное: откуда взялась строка. В ответе модели появится «деталь принята 03.05.2024», а проверить, действительно ли так написано в акте, будет нечем, кроме веры в OCR.

Решение — хранить рядом три вещи: исходный файл, извлечённый текст и происхождение каждого фрагмента. Модель документа Docling устроена в этом духе: DoclingDocument хранит текст, таблицы, иерархию и сведения о происхождении, а координаты элементов доступны, если они были получены при разборе. Условие «если получены» важно: не стоит строить интерфейс в расчёте на рамку вокруг строки, пока вы не убедились, что для ваших файлов и выбранного режима разбора координаты действительно приходят.

Минимальная запись для каждого фрагмента, который уходит в индекс, выглядит так:

  • идентификатор исходного файла и его контрольная сумма, чтобы замена файла не оставила «висящих» ссылок;
  • номер страницы;
  • координаты области на странице, если они есть, а если нет — хотя бы номер страницы;
  • способ получения текста: родной слой или OCR (и каким движком), потому что доверять им нужно по-разному;
  • сам текст фрагмента.

Отметка «OCR или слой» полезнее, чем кажется. Когда пользователь жалуется на странный ответ, первый вопрос — откуда этот текст. Если это слой из PDF, то ошибка, скорее всего, в самом документе. Если OCR — смотрите на изображение. Без пометки вы гадаете.

Об инфраструктуре: исходные файлы и извлечённый текст лежат в разных местах, и это нормально. Файлы — в обычном хранилище (на VPS это может быть каталог на NVMe или файловый сервис вроде Nextcloud, который в каталоге MAATRIX предлагается с установкой; наличие и ОС проверяйте в форме заказа). Текст и метаданные — в базе или векторном индексе. Главное, чтобы из записи индекса всегда можно было однозначно прийти к файлу и странице. Общую схему развёртывания RAG на своём сервере мы разбирали отдельно: как поднять RAG по своим документам на сервере.

Две разные беды: «не прочитали» и «не нашли»

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

Возьмите несколько подмен, которые реально встречаются в русских сканах:

Типовые подмены и их последствия
В оригиналеРаспознаноЧитается человеком?Находится поиском?
4007-1240О7-12 (буква «О»)да, без раздумийнет: другой токен
03.05.202403,05.2024дазависит от нормализации дат
дефекты не обнаруженыдефекты обнаруженыда, но смысл обратныйнаходится даже по неверному запросу
СергеевCергеев (латинская «C»)данет по точному совпадению

Первая и четвёртая строки — случаи, когда глаз ничего не замечает, а индекс промахивается. Это беда поиска: текст «в целом правильный». Лечится она на стороне индекса, а не повторным распознаванием: нормализацией символов-двойников (латинская «C» и кириллическая «С» приводятся к одному виду), а для идентификаторов вроде номеров деталей — дополнительным нормализованным полем, в котором «О» между цифрами заменена на «0». Оригинальную строку при этом оставляют: нормализованное поле служит только для поиска.

Вторая строка — промежуточный случай. Если вы разбираете даты регулярным выражением, запятая вместо точки ломает разбор. Если даты остаются обычным текстом, поиск «03.05.2024» просто не совпадёт. Простое правило: даты и числа в метаданные записывайте в разобранном виде там, где это можно сделать надёжно, а непонятные оставляйте как есть и помечайте.

Третья строка принципиально иная: потеряно отрицание, и это беда распознавания, которую индексом не исправить. Хуже того, поиск в этом случае работает «слишком хорошо»: запрос «дефекты обнаружены» уверенно вернёт именно этот фрагмент. Здесь помогают только проверка на размеченных примерах и возможность быстро посмотреть оригинал. Слова с отрицанием («не», «нет», «без», «отсутствует») стоит держать под особым наблюдением: их потеря почти не меняет вид строки и полностью меняет вывод.

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

Контрольная выборка: пятьдесят строк, которые знают ответ

Чтобы не спорить по ощущениям, соберите небольшой набор контрольных документов и проверяйте на нём каждое изменение: другой движок, новый фильтр, другой режим. Такой набор — рабочий инструмент вашей команды, а не часть какого-либо OCR-пакета. Хорошая выборка маленькая и пёстрая, чтобы вручную разметить её за вечер.

Что в неё включить:

  • по несколько страниц из каждой группы, выделенной на первом шаге: родной слой, скан без слоя, смешанная страница, фото;
  • строки с цифровыми идентификаторами, где есть ноль, буква «О», единица и «l», тройка и «З»;
  • даты в разных написаниях: «03.05.2024», «3 мая 2024 г.», «03.05.24»;
  • предложения с отрицанием, в том числе у края строки и рядом с печатью;
  • фамилии и названия с буквами, у которых есть латинские двойники: А, В, Е, К, М, Н, О, Р, С, Т, Х;
  • мелкий шрифт, сноски, колонтитулы.

Для каждой страницы руками перепишите эталонный текст нужных строк. Пятьдесят-сто строк — достаточно, чтобы увидеть картину, и мало, чтобы устать. Заодно запишите пять-десять вопросов, на которые документ должен отвечать: «Когда принята деталь 4007-12?», «Были ли обнаружены дефекты?». У каждого вопроса известна страница с ответом.

Что измерять в первую очередь

Измерения делятся на два слоя, и их нужно считать раздельно:

  1. Качество текста. Сравните распознанные строки с эталоном. Считайте долю символов с ошибками, но обязательно отдельно разберите критичные классы: цифры внутри идентификаторов, даты, пропавшие отрицания. Общий процент ошибок может быть небольшим, а в критичных местах ошибка стоять в каждой третьей строке — среднее эти провалы скрывает.
  2. Качество поиска. Для каждого контрольного вопроса проверьте, попала ли правильная страница в первые несколько результатов. Если нет — смотрите, прочитан ли нужный фрагмент верно. Если прочитан верно, а поиск промахнулся, причина в индексе или в запросе. Если прочитан неверно — причина в распознавании.

Такая раскладка превращает расплывчатое «RAG плохо работает на сканах» в таблицу из конкретных причин: столько-то промахов из-за подмены «О» и «0», столько-то из-за потерянного отрицания, столько-то из-за разбитых строк при перекосе. Для условного примера: пусть на сорока вопросах правильная страница не нашлась в двенадцати. Если в девяти из них нужный фрагмент прочитан неправильно, то главный рычаг — предобработка и выбор режима. Если в девяти текст верный, а поиск не нашёл, рычаг — нормализация и параметры индекса. Это условные цифры, показывающие способ рассуждения; ваша таблица будет другой.

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

Как это выглядит для пользователя: ответ со ссылкой на скан

Всё, что выше, нужно ради одной простой возможности. Когда система отвечает «деталь принята 03.05.2024, дефектов не обнаружено», рядом должна быть ссылка «открыть страницу 7 акта». По клику человек видит исходное изображение, а если координаты доступны — подсвеченную область со строкой. Секундного взгляда достаточно, чтобы заметить, что на скане написано «обнаружены», а не «не обнаружены».

Для этого интерфейсу нужно немного: идентификатор файла, номер страницы и, при наличии, рамка. Если координат нет, открывайте страницу целиком — это всё равно в разы лучше, чем перечитывать документ на сорок страниц. Сомнительные фрагменты имеет смысл помечать заранее: те, что получены через OCR, где в строке много цифр или есть отрицание, показывать с пометкой «распознано автоматически, проверьте по скану».

Распознавание никогда не станет безошибочным, особенно на русских архивах с бледными копиями и штампами поверх текста. Но система, которая признаёт эту неопределённость и в любой момент отправляет читателя к самой странице, оказывается надёжнее той, что молча выдаёт гладкие ответы. Когда в ответе стоит дата или номер детали, а текст вызывает сомнение, один клик на исходный скан решает вопрос быстрее любого спора с моделью.