Сто страниц сканов и один оператор: когда распознавание текста дешевле, чем перепечатать
Перепечатывание пачки сканов — это не «пара часов», а стабильная статья расходов и источник опечаток в реквизитах. Считаем, что отдать машине, что оставить человеку и где проходит граница.
В бэкофисе логистической компании каждое утро в общий ящик падает от восьмидесяти до полутора сотен сканов: акты, транспортные накладные, доверенности, анкеты новых водителей. Оператор открывает картинку, находит глазами номер документа, ИНН, сумму и дату, перепечатывает их в учётную систему, закрывает, открывает следующую.
Восемь месяцев назад в накладной вместо ИНН 7707083893 в базу уехало 7707083839. Две последние цифры поменялись местами — обычная опечатка усталого человека на семидесятом документе за день. Платёж ушёл не туда, вернулся через одиннадцать дней, контрагент успел выставить неустойку. Разбирались две недели.
После этого в компании ввели двойной ввод: реквизиты набивают два человека независимо, система сравнивает. Опечатки почти исчезли. Стоимость приёмки документов выросла ровно вдвое.
Это и есть та развилка, на которой обычно вспоминают про распознавание текста. Дальше — как оно устроено сейчас, во что обходится ручная приёмка в часах и где у машины заканчиваются полномочия.
Где именно течёт
Разговор про оцифровку почти всегда начинается с фразы «нам бы сканы автоматически загонять в базу». Под ней прячутся три совершенно разные боли, и лечатся они по-разному.
Реквизиты набивают руками, и в них опечатки
- Что происходит
- ИНН, номер счёта, номер накладной, сумма. Пятнадцать-двадцать цифр подряд, которые человек переносит с картинки на клавиатуру, глядя попеременно в два места.
- Во что обходится
- Опечатка в расчётном счёте — это платёж не туда и от нескольких дней до пары недель на возврат. Опечатка в ИНН — непрошедшая сверка и ручной разбор в конце квартала.
- Чем закрывается
- Распознавание отдаёт текст страницы за секунду, и оператору остаётся сравнить две строки, а не набрать одну. Сравнивать глазами человек умеет гораздо лучше, чем набирать.
Приёмка документов упирается в одного человека
- Что происходит
- Сто двадцать сканов в день, один оператор, шесть часов чистого времени. Заболел — очередь копится, вышел — разгребает три дня.
- Во что обходится
- Полдня простоя в закрытии месяца и авральные переработки в конце квартала. Плюс невозможность вырасти по объёму без найма ещё одного человека.
- Чем закрывается
- Машина берёт на себя набор, человек — контроль. Пропускная способность одного оператора вырастает в разы, и потолок сдвигается без найма.
Данные в базе есть, но искать по ним нельзя
- Что происходит
- Архив из тридцати тысяч JPEG. Найти договор с конкретным контрагентом можно только по имени файла, а называли их как придётся.
- Во что обходится
- Юрист тратит полтора часа на поиск одного документа. На запрос от налоговой архив поднимают вручную неделю.
- Чем закрывается
- Распознанный текст ложится рядом с картинкой в поисковый индекс. Дальше архив ищется обычным поиском по подстроке.
Сколько это стоит в часах и когда окупается
Считать «экономию от OCR» в процентах бессмысленно — цифра получится любой. Считать надо в секундах на поле, и допущения я назову вслух, чтобы вы подставили свои.
Замерять просто: посадите оператора с секундомером на двадцать документов. У большинства команд, которые я видел, получается близко к цифрам ниже, но ваши будут другими.
| Операция | Руками | С распознаванием |
|---|---|---|
| Открыть скан, найти нужное поле глазами | 5–8 сек | 5–8 сек (никуда не делось) |
| Перенести 12–15 цифр в форму | 20–40 сек | — |
| Сверить набранное с картинкой | 10 сек | 7–10 сек |
| Запрос к сервису на страницу | — | около 1 сек |
| Итого на документ из 4 полей | около 3,5 минут | около 1 минуты |
Сто двадцать документов в день — это примерно семь часов ручной работы против двух. Пять освободившихся часов на человека в день, и это единственная цифра в статье, которую стоит воспринимать всерьёз, потому что она следует из арифметики, а не из обещаний.
Стоимость запросов при этом остаётся далеко позади стоимости часов. Одна страница — один запрос, сто двадцать документов по одной-две страницы — меньше трёхсот запросов в день. Цена одного запроса — десятки копеек, актуальная цифра на странице тарифов: она меняется, а статья остаётся. Триста запросов в день — это порядка полутора сотен рублей, то есть примерно стоимость получаса работы человека против пяти сэкономленных часов. Разрыв не астрономический, но десятикратный, и он устойчив к тому, что цена подрастёт.
Как это встроить, чтобы работало
Порядок такой, и последний шаг важнее первых.
Шаг первый: приведите страницу в разумный вид. На вход метода уходит картинка в Base64, и после декодирования она должна укладываться в десять мегабайт. Скан А4 в 300 dpi весит 2–5 МБ и проходит спокойно, так что бороться за килобайты не нужно. Полезно другое: не гнаться за разрешением вверх. 600 dpi в цвете там, где хватает 200–300 в сером, не добавляет точности — этот запас нужен мелкому шрифту в сносках, а не основному тексту, — зато удлиняет обработку каждой страницы, и на пакете из тысячи листов разница уже измеряется часами. Заодно выпрямите перекос: кривой скан с тенью от сгиба портит результат сильнее, чем что-либо ещё.
Шаг второй: отправьте страницу в сервис «Распознавание текста». Целиком, одним
запросом. Карта зон с координатами прямоугольников под каждый тип
документа не нужна: разметку макета сервис строит сам. Резать страницу на куски имеет
смысл в одном случае — когда конкретное поле на плохом скане читается неуверенно и вы
хотите переспросить по нему отдельно, указав mode: "digits" там, где
ожидаются только цифры. Тогда цифра «0» перестаёт превращаться в букву «O».
Шаг третий: вытащите поля из Markdown, а не из пикселей. В ответе приходит текст с сохранённой структурой, и зацепки в нём смысловые: подпись «ИНН» и число за ней, шапка столбца «Сумма» и колонка под ней. Такой разбор переживает смену вёрстки бланка, чего нельзя сказать про координаты прямоугольника, которые ломались от каждого нового шаблона накладной.
Шаг четвёртый — и он обязательный: проверьте результат тем, что вы про поле знаете. Сервис отдаёт текст и не отдаёт оценку уверенности, поэтому доверие к ней вы строите сами. ИНН юрлица — ровно 10 цифр со сходящейся контрольной суммой. Расчётный счёт — 20 цифр и своя контрольная сумма с БИК. Дата — разбирается в календарную. Сумма — сходится с суммой строк. Каждая такая проверка стоит вам нисколько и ловит именно те ошибки, которые делает OCR: перепутанный символ, лишний или потерянный.
Всё, что не прошло проверку, уходит человеку. Всё, что прошло, — тоже уходит человеку, но уже в режиме «посмотри и нажми Enter».
Код: страница и один запрос
Все примеры работают с публичным демо-ключом ak_sandbox_demo_mockdata_v1 —
копируйте и запускайте, регистрация не нужна. На демо-ключе API отдаёт моки:
содержимое выдумано, но форма ответа настоящая — заголовок, абзацы и
Markdown-таблица с сошедшимся итогом, ровно как отдаёт боевой движок. Это сделано
намеренно: разбор ответа, написанный на песочнице, должен работать и с личным ключом,
а не разваливаться при первом реальном запросе. Ответы детерминированы — одна и та же
картинка всегда даёт один и тот же результат, поэтому на песочнице удобно писать тесты
конвейера. Настоящее распознавание работает только с личным ключом.
Метод один: POST /api/ocr/image-to-text. В теле — JSON с картинкой в Base64.
| Поле запроса | Тип | Что делает |
|---|---|---|
image |
строка, обязательное | Картинка в Base64. Принимается и «голая» строка, и data-URL вида data:image/png;base64,... — префикс сервис срежет сам |
mode |
строка, по умолчанию "auto" |
Режим распознавания: auto — сервис сам различает строку и страницу; document — страница целиком со структурой; table — только таблица; formula — формула в LaTeX; line — короткая строка; digits — короткая строка из одних цифр. Указывать явно стоит, когда переспрашиваете по вырезанному полю или когда важна предсказуемая стоимость |
# Картинка кодируется в Base64 и уходит в теле запроса.
# -w0 нужен, чтобы base64 не разбил строку переносами.
IMG=$(base64 -w0 page_01.jpg)
curl -X POST "https://atlorium.com/api/ocr/image-to-text" \
-H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
-H "Content-Type: application/json" \
-d "{\"image\":\"$IMG\"}"
import base64
import requests
BASE = "https://atlorium.com"
HEADERS = {"Authorization": "Bearer ak_sandbox_demo_mockdata_v1"}
def recognize(img_bytes: bytes, digits_only: bool = False) -> str | None:
"""Возвращает распознанный текст либо None, если распознать не удалось."""
resp = requests.post(
f"{BASE}/api/ocr/image-to-text",
json={
"image": base64.b64encode(img_bytes).decode(),
"mode": "digits" if digits_only else "auto",
},
headers=HEADERS,
timeout=30,
)
# 503 — источник недоступен. Кредиты при этом не списываются,
# так что повторить запрос позже ничего не стоит.
if resp.status_code == 503:
return None
resp.raise_for_status()
data = resp.json()
# recognized=false означает «читаемого текста не нашли». Это не ошибка
# запроса и не повод для retry: тот же файл даст тот же результат.
if not data["recognized"]:
return None
# truncated=true — страница длиннее предела одного ответа, текст обрезан.
# Внешне такой ответ неотличим от полного, поэтому проверять поле обязательно:
# молча принять обрезанный документ хуже, чем не принять никакой.
if data.get("truncated"):
send_to_operator("Страница распознана не полностью")
return None
return data["text"]
with open("page_01.jpg", "rb") as f:
page = recognize(f.read())
if page is None:
send_to_operator("Страница не распознана")
else:
# Ищем реквизит по подписи в тексте, а не по координатам на картинке.
inn = find_after_label(page, "ИНН")
if inn is None or not valid_inn_checksum(inn):
send_to_operator(f"ИНН не подтверждён: {inn}")
else:
save(inn)
using System.Net.Http.Json;
using System.Text.Json;
var http = new HttpClient { BaseAddress = new Uri("https://atlorium.com") };
http.DefaultRequestHeaders.Add("Authorization", "Bearer ak_sandbox_demo_mockdata_v1");
var bytes = await File.ReadAllBytesAsync("page_01.jpg");
var payload = new
{
image = Convert.ToBase64String(bytes),
mode = "document", // страница целиком: и цифры, и текст
};
var resp = await http.PostAsJsonAsync("/api/ocr/image-to-text", payload);
// 503 — сервис недоступен или перегружен, списания не было: можно повторить позже.
if (resp.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
return null;
resp.EnsureSuccessStatusCode();
var card = await resp.Content.ReadFromJsonAsync<JsonElement>();
var recognized = card.GetProperty("recognized").GetBoolean();
// text приходит null, когда recognized=false, — читать его без проверки нельзя.
var text = recognized ? card.GetProperty("text").GetString() : null;
// format сообщает, ЧТО лежит в text: "markdown" (страница с разметкой),
// "plain" (короткая строка) или "latex" (формула). Выбирать разбор по этому
// полю надёжнее, чем угадывать по содержимому.
var format = card.GetProperty("format").GetString();
// units — во сколько единиц работы обошёлся запрос: по ним он и тарифицирован.
// Короткая строка это всегда 1, страница — столько, сколько на ней областей.
var units = card.GetProperty("units").GetInt32();
// truncated=true — текст обрезан по пределу длины, страница неполная.
var truncated = card.GetProperty("truncated").GetBoolean();
var elapsedMs = card.GetProperty("elapsedMs").GetInt64();
{
"recognized": true,
"format": "markdown",
"truncated": false,
"text": "## Товарная накладная № 4417 от 12.03.2026\n\nПоставщик: ООО «Ромашка», ИНН 7707083893\n\n| № | Наименование | Кол-во | Сумма |\n|---|---|---|---|\n| 1 | Короб 600x400 | 120 | 43 200,00 |\n| 2 | Стретч-плёнка | 15 | 8 250,00 |\n\nИтого: 51 450,00",
"elapsedMs": 1180
}
# Читаемого текста не нашлось — плата за такой запрос не берётся:
{
"recognized": false,
"text": null,
"format": "plain",
"truncated": false,
"elapsedMs": 940
}
Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории image-ocr-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.
Полей в ответе пять, и одну вещь стоит принять как данность при проектировании: вся
структура страницы приезжает разметкой внутри text, отдельных полей под
координаты слов или список вариантов в ответе нет. Что делать с каждым:
| Поле ответа | Что означает | Что делать |
|---|---|---|
recognized: true |
Текст распознан, он лежит в text |
Вытащить нужные реквизиты по подписям, прогнать через проверку формата, затем показать оператору на сверку |
recognized: false |
Читаемого текста на изображении не нашлось. text при этом null |
Отправить страницу человеку на ручной ввод. Повторять запрос смысла нет — картинка та же. Списания за такой запрос не происходит |
text |
Содержимое страницы с разметкой Markdown | Не сохранять как есть в реквизиты. Разобрать, убрать пробелы по краям, проверить длину и контрольную сумму |
format |
"markdown" — страница с разметкой; "plain" — простая строка; "latex" — формула |
Выбирать по нему ветку разбора вместо угадывания по содержимому. Список значений может пополниться, поэтому незнакомое значение разумно обрабатывать как plain |
mode |
Режим, в котором изображение распознано на самом деле | Совпадает с запрошенным, кроме auto — там видно решение сервиса. Если результат оказался не тем, что ожидалось, смотреть надо сюда: почти всегда автоопределение приняло страницу за строку или наоборот |
units |
Единицы работы, в которые обошёлся запрос | По ним запрос и тарифицирован. Короткая строка — всегда 1, страница — столько, сколько на ней распознано областей. Удобно сверять расход по пакету, не заглядывая в личный кабинет |
truncated |
true — текст обрезан по пределу длины либо по потолку числа областей, страница распознана НЕ полностью |
Проверять обязательно. Обрезанный ответ внешне неотличим от полного, и без этой проверки потеря части документа пройдёт незамеченной. Такую страницу отправлять на ручную сверку |
elapsedMs |
Сколько заняло распознавание — вместе с ожиданием очереди на GPU, а не только сам инференс | Складывать в метрики. Именно потому, что очередь входит в цифру, по ней и подбирается параллелизм: пополз вверх при тех же картинках — конвейер упёрся в пропускную способность, и наращивать потоки дальше бесполезно |
Из кодов ошибок в конвейере важны три. 400 — картинка не передана, битый Base64 или файл больше десяти мегабайт: чинится на вашей стороне, кредит не тратится. 429 — упёрлись в лимит запросов, конвейер надо притормозить. 503 — движок временно недоступен или перегружен, и вот здесь важная деталь: деньги за такой запрос не списываются. Пакет можно спокойно поставить в очередь и прогнать через полчаса.
Отдельно про темп. На клиента отведено 60 запросов в минуту — ровно страница в
секунду, и это осознанно много: сервис задуман под пакетную оцифровку, где меньший темп
просто бессмысленен. Но параллелить сверх этого не нужно даже не из-за 429: страницы
считает одна видеокарта, и запросы сверх её пропускной способности встают в очередь.
Практический вывод для конвейера — увеличивать параллелизм, пока растёт скорость
пакета, и останавливаться, когда начинает расти elapsedMs. Момент, когда
время ответа поползло вверх при тех же картинках, и есть точка насыщения; дальше вы
добавляете не производительность, а длину очереди.
И одна честная оговорка про песочницу. Структуру ответа мок повторяет точно, но
вашу картинку он не читает — использует её лишь как зерно генератора, поэтому
текст в ответе никак не связан с тем, что на изображении. И мок всегда отвечает
recognized: true с truncated: false: ветки
recognized: false и обрыва по длине на демо-ключе воспроизвести нельзя,
тестировать их придётся подстановкой ответа в своём коде.
Чего распознавание не умеет
Раздел, который стоит прочитать до того, как обещать результат руководству. Ниже — то, чего в сервисе нет, и никакие параметры этого не добавят.
Рукописный текст мы не заявляем. Модель рассчитана на печатные символы. Подпись, заполненную от руки строку в анкете, пометки на полях она может прочитать, а может выдать правдоподобную ерунду, и отличить одно от другого по ответу вы не сможете. Строить на рукописном вводе процесс не стоит: там, где в бланке есть рукописные поля, планируйте ручную сверку по умолчанию.
Оценки уверенности нет. Есть только recognized: да или нет.
Промежуточного «распознал, но не уверен» не существует, и порог «отправить оператору при
уверенности ниже 0,8» построить не на чем. Роль этого порога у вас играют проверки
формата и контрольные суммы реквизитов — их придётся написать. Это не отговорка: для
ИНН, счёта и суммы контрольная сумма надёжнее любого внутреннего скора модели, потому
что проверяет само значение, а не то, насколько уверенно оно прочиталось.
Юридической значимости у результата нет. Распознанный текст — это удобная копия для поиска, ввода и сверки, а не документ. Для спора, суда и проверки основанием остаётся оригинал, и заменить его выгрузкой из конвейера нельзя.
Тяжёлые PDF — отдельная история. Сама модель PDF на вход принимает, но метод
image-to-text работает с растровой картинкой, поэтому многостраничный файл
вы рендерите в страницы у себя и отправляете постранично. Это не временная недоделка, а
осознанная граница: постраничный рендер на своей стороне даёт вам контроль над dpi и
цветом — то есть над скоростью и ценой пакета. Для случаев, где важны электронные
подписи, слои и сложная вёрстка, планируется отдельный сервис — обещать его как готовый
я не буду, потому что его пока нет.
Сервис не подскажет, что это за документ. Классификация типа, сопоставление контрагента с базой, решение «проводить или отклонить» — всё это остаётся вашему коду. Модель отвечает на один вопрос: «какой текст на этой странице и как он там расположен». Что этот текст значит для вашего бизнес-процесса, она не знает.
И общий вывод, который стоит держать в голове при планировании. Полностью безлюдной приёмку документов OCR не делает — по крайней мере, там, где ошибка в реквизитах стоит денег. Он снимает с человека набор символов, самую утомительную и самую ошибкоопасную часть работы, и оставляет ему сверку и решения. Если считать успехом именно это, проект окупится за месяцы. Если ждать полного исчезновения оператора — не окупится никогда.
Частые вопросы
Частые вопросы
Как распознать текст со скана документа через API?
Отправьте POST на /api/ocr/image-to-text с картинкой страницы в Base64 в поле image. В ответ придёт recognized и содержимое страницы в поле text с разметкой Markdown: заголовки, абзацы и таблицы сохраняют структуру. Резать страницу на зоны заранее не нужно, макет модель размечает сама.
Распознаются ли таблицы в накладных и актах?
Да. Движок распознаёт таблицы и формулы и возвращает их разметкой Markdown с сохранением столбцов и порядка чтения. Отдельно вырезать ячейки не требуется. Значения из таблицы всё равно стоит проверить у себя: сумма строк должна сходиться с итогом.
Можно ли отправить PDF-файл?
Нет, метод image-to-text принимает растровое изображение страницы. Многостраничный PDF нужно отрендерить в картинки на своей стороне и отправлять постранично — заодно вы сами управляете dpi и цветом, а значит скоростью и ценой пакета. Для тяжёлых файлов с электронными подписями и сложной вёрсткой планируется отдельный сервис.
Что делать, если текст не распознан?
Ответ придёт с recognized: false и text: null, и плата за такой запрос не взимается. Повторять тот же запрос бесполезно — картинка не изменилась. Правильная реакция: отправить страницу оператору на ручной ввод, а сам файл посмотреть глазами: чаще всего дело в перекосе, тени или слишком низком разрешении.
Есть ли в ответе оценка уверенности распознавания?
Нет. В ответе есть recognized, text, format, truncated и elapsedMs — промежуточного значения между «распознал» и «не распознал» среди них нет, поэтому порог по уверенности построить не на чем. Его роль выполняют проверки формата поля и контрольные суммы реквизитов — ИНН, расчётного счёта, итоговой суммы.
Куда уходят мои документы при распознавании?
Никуда за пределы нашей инфраструктуры. Под сервисом работает наша модель распознавания, развёрнутая на собственном сервере Atlorium, а не сторонний API распознавания. Страница обрабатывается у нас и результат возвращается в том же HTTP-ответе.
Какой максимальный размер картинки?
Десять мегабайт после декодирования из Base64. Скан А4 в 300 dpi укладывается в этот предел с запасом, так что специально сжимать страницу не нужно. Поднимать разрешение выше 300 dpi смысла нет: распознаётся не лучше, а времени на обработку уходит больше — на пакете это заметно.
Сколько стоит распознавание и платится ли неудачная попытка?
Первые пять распознаваний в сутки бесплатны (гостю — одно), дальше оплата идёт по факту запроса, без подписки и абонентской платы; актуальная цена указана на странице тарифов. Списывается только успешное распознавание: при recognized: false и при ошибке 503, когда сервис недоступен или перегружен, деньги не берутся.
Можно ли попробовать до регистрации?
Да, двумя способами. Публичный демо-ключ ak_sandbox_demo_mockdata_v1 работает без аккаунта и отдаёт моки — сгенерированные, но детерминированные ответы, на которых удобно писать тесты конвейера; своё изображение он не читает. А чтобы посмотреть настоящее распознавание на своём документе, есть бесплатная суточная квота: одно распознавание гостю и пять авторизованному пользователю.
Сервисы из этой статьи
OCR «картинка → текст»: печатный текст, таблицы и формулы со скана или фото через REST API; плата только за успешное распознавание
Попробовать прямо сейчас — без регистрации
Демо-ключ ak_sandbox_demo_mockdata_v1 — публичный и общий для всех.
С ним API отвечает моками: данные правдоподобные, но сгенерированные,
и они не меняются от запроса к запросу — на них удобно писать тесты.
Настоящие данные приходят с личным ключом.
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
https://atlorium.com/openapi/ocr_ru.json