OCR — это распознавание текста с изображения. Применительно к российским накладным звучит просто: «возьмите фото, верните таблицу позиций». На практике это до сих пор одна из самых хрупких задач в B2B-автоматизации: ТОРГ-12 пишут от руки, штампы переезжают на текст, лента кассового аппарата затирает суммы, страницы накладной фотографируют под наклоном с подсветкой от окна.
За последние два года рынок изменился сильнее, чем за десять до этого: vision-LLM (Claude Sonnet 4.6, GPT-4o vision, Gemini 1.5 Pro) научились читать целую накладную в один заход — без отдельной OCR-стадии. В этой статье мы разберём, как именно это работает у нас в ScanFlow — на цифрах из реального продакшена.
Проблема: почему обычный OCR проигрывает на накладных
Классическая OCR-задача — это плоский текст: книга, договор, абзацы. Накладная — это таблица, в которой важен не только текст, но и геометрия: какая ячейка относится к колонке «количество», а какая — к «итого». Если потерять колонку, числа поплывут.
Типовые ошибки на накладных, которые мы видим в нашем потоке:
- SKU прочитан как количество. В ТОРГ-12 артикулы выглядят как «113393» — длинное число рядом с позицией. Без понимания структуры таблицы OCR парсит его в графу «количество», а реальное «10» уходит в графу «цена». Накладная на 14 позиций превращается в неправдоподобный итог в миллион рублей.
- Перенос между страницами. Многостраничная накладная: на странице 1 — 15 позиций, на странице 2 — продолжение нумерации, а на странице 3 — только итоги и подпись. Если соединять их «текстом», ИТОГО будет посчитан дважды, а позиции с переноса потеряются.
- Своп НДС и итога. Колонки «без НДС / НДС / с НДС» близко друг к другу, OCR путает их местами в строках, где НДС не выписан явно. Так в учёт попадает завышенная или заниженная сумма налога.
- Печать поверх текста. Штамп поставщика смещается на цену — и распознаётся как часть числа.
Как обычно решают: классическая цепочка
Стандартная архитектура «OCR-стартапа» 2019-2022 годов выглядела так:
- Препроцессинг изображения — deskew (выравнивание наклона), denoise (шумоподавление), бинаризация. Обычно через OpenCV или Sharp.
- Сам OCR — Google Cloud Vision / Yandex Vision OCR / Tesseract — даёт сырой текст с bounding-box'ами.
- Layout analysis — поиск таблиц: где границы, где заголовки колонок. Чаще всего регулярки и эвристики.
- Парсер позиций — извлекает кортежи
(название, qty, price, total)из распознанных строк таблицы, опять же regex-ами. - Валидация —
qty × price ≈ totalдля каждой строки; если не сходится — флаг для ручной проверки.
Эта цепочка работает, но каждый шаг — точка отказа. Google Vision отлично читает буквы, но не понимает, что «113393» — это артикул. Регулярки парсера ломаются на любом нестандартном формате таблицы. Layout analysis на 4-страничной накладной с переносом колонок — это отдельная боль.
Подводный камень из нашей истории: в нашем регулярном парсере живёт массив skipKeywords — список слов вроде «образец», «заполнения», «платёжного». Каждое слово там блокирует реальный false-positive, пойманный на проде. Удалить любое — значит вернуть конкретную ошибку парсинга. Это типично для regex-based OCR: эвристики растут как бородавки и хрупки к любым изменениям формата.
Как это работает в ScanFlow: один вызов вместо четырёх
Сегодня в продакшене OCR работает в одном API-вызове: фото уходит напрямую в Claude Sonnet 4.6 (vision-режим), модель возвращает структурированный JSON со всеми позициями накладной. Отдельной OCR-стадии больше нет.
Что происходит под капотом
- Препроцессинг — только ориентация. Sharp поворачивает изображение по EXIF, а Claude Haiku 4.5 на маленьком 400-пиксельном превью определяет правильную ориентацию (документ может быть боком или вверх ногами). Без этого vision-модели сильно галлюцинируют на повёрнутом тексте — это самая дешёвая и эффективная оптимизация во всём пайплайне.
- Vision-запрос с typed schema. Claude получает картинку и инструкцию вернуть JSON по схеме
{invoice_number, supplier, items: [{name, qty, unit, price, total, vat_rate}]}. Никаких регулярок, никакого layout-analysis — модель сама «видит» таблицу как таблицу. - Валидация в коде. Для каждой строки проверяем
qty × price ≈ total ± 1%. Если расходится — флагitems_total_mismatch, накладная не уходит в 1С автоматом, ждёт оператора. - Маппинг номенклатуры. Названия типа «мол. 3.2% Простоквашино» сопоставляются с каталогом 1С через Fuse.js fuzzy match + Claude LLM-маппер. Это уже не OCR, но часть пайплайна — и без неё распознанная позиция бесполезна.
Что мы видим в продакшене
Сервис ещё в открытой бете, репрезентативной статистики ещё нет — но из ежедневного использования на одном пайплайне видно так:
- Время на одну страницу: 1–3 секунды для типового ТОРГ-12 на 5–10 позиций. Многостраничные накладные с десятками позиций — 30–120 секунд (Claude обрабатывает все изображения в одном запросе).
- Стоимость API: единицы центов за документ на Claude Sonnet 4.6, дешевле на Haiku 4.5. Точные цифры зависят от тарифа Anthropic и токенов в накладной.
- Многостраничные: алгоритм объединения склеивает страницы по общему номеру и поставщику через
findRecentByNumberв parser'е — работает в большинстве случаев, иногда нужно поправить руками. - Ловушка OCR-ошибок: кросс-валидация
qty × price ≈ totalиsum(items.total) ≈ invoice.totalловит, по нашим прикидкам, около 30% мелких ошибок до того, как накладная уходит в 1С.
Главный сдвиг — не «точность OCR стала выше». Главный сдвиг — модель понимает накладную как документ, а не как пиксели.
Почему fallback-цепочка всё ещё в коде
В src/ocr/ocrManager.ts по-прежнему живут Google Vision и Tesseract — мы их не выбросили после миграции на Claude. Причины утилитарные:
- Network-сбои до Anthropic API случаются. Когда падает
api.anthropic.com, лучше отдать оператору пусть менее точный, но непустой результат из Google Vision + regex-парсинга, чем 500-ю ошибку. На наших цифрах такое случается несколько раз в месяц на одного потребителя API. - Дешёвая фолбэк-валидация. Если новый Claude-ответ выглядит подозрительно (например, total на порядок меньше суммы позиций), мы можем сверить с тем, что увидел Google Vision в исходном тексте.
- Регуляторно полезно иметь deterministic-парсер — точку доверия с воспроизводимым поведением, не зависящую от внешнего сервиса. Это важно для аудита.
Что важно учесть
Если вы строите свой OCR-пайплайн на vision-LLM, эти грабли мы уже наступили — не повторяйте.
Не доверяйте суммам, считайте их сами
Claude иногда округляет числа «для красоты»: возвращает 3 500.00, когда в накладной стоит 3 482.50. Всегда сверяйте qty × price = total и sum(items.total) = invoice.total. На наших данных эта проверка ловит около 30% мелких ошибок до того, как они уходят в учётную систему. Дешёвая страховка, которая окупается с первого инцидента.
Считайте rate-limits и стоимость заранее
Claude Sonnet 4.6 vision при 200 запросах/мин (стандартный лимит на старте) выдерживает примерно двух одновременных пользователей, делающих batch-загрузку накладных. Если у вас планируется массовая обработка — заранее запрашивайте увеличение rate-limit у Anthropic, очередь иначе быстро встанет.
Кешируйте ориентацию
Определение ориентации через Claude Haiku — это лишний API-вызов на каждой картинке. Мы кешируем результат по hash файла; повторно отправленная накладная не платит дважды. Мелочь, но на batch-загрузке экономит десятки центов в неделю.
Если бюджет важен: используйте Claude Haiku 4.5 вместо Sonnet для простых накладных (одна страница, типографский шрифт, никаких рукописных правок). Стоимость API снижается примерно в 10× при сопоставимой точности для типографских документов; на рукописных пометках Sonnet всё-таки чувствует контекст лучше. Переключение — в Настройки → Анализатор, поле claude_model: впишите любой Anthropic model ID.
FAQ
Можно ли подключить ваш OCR к своему 1С через API?
Да, у нас есть REST с X-API-Key: POST /api/upload принимает файл и возвращает structured JSON с распознанными полями. Документацию интеграции получите после регистрации, она же приходит вместе со скачиваемой внешней обработкой .epf для 1С:УНФ.
Что с рукописными накладными?
Печатный текст vision-LLM читает уверенно. Рукописные правки от руки поверх печатной формы (типичный случай в общепите) — заметно хуже, особенно если поправлена сумма или количество: всегда проверяйте такие позиции глазами и опирайтесь на флаг items_total_mismatch. Полностью рукописная накладная — нестандартный кейс, мы его специально не оптимизируем; для такого лучше переснять с лучшим освещением или ввести вручную в 1С.
А если ваш сервис ляжет — что с моими данными?
Распознанные документы хранятся в MySQL на серверах в РФ; дампы делаем mysqldump'ом вручную перед апдейтами и по запросу клиента (автоматический cron в roadmap'е). Фото исходников хранятся 90 дней, потом удаляются автоматически (см. RETENTION_DAYS=90 в photoRetention.ts). По запросу выгружаем всё в JSON — никакого vendor-lock. Бизнес-критичные данные (контрагенты, номенклатура) синхронизируются с вашей 1С — мы тут только промежуточный слой.
Что дальше
OCR — это вход пайплайна. Выход — интеграция с учётной системой и банком. Дальше у нас есть отдельные разборы:
- От фото накладной до приходной в 1С:УНФ за 3 секунды — что забирает 1С через REST, как маппится номенклатура, как считается НДС по дате.
- ТОРГ-12: какие графы важны и где OCR ошибается чаще всего — углубление по самой популярной российской форме накладной.
Хотите попробовать на своих документах — регистрируйтесь, сейчас бесплатно во время беты.