Как устроен ИИ-сканер чеков: от фото до позиций, валюты и перевода

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

    «Сфоткал чек – ИИ распознал позиции» звучит как одна кнопка. На деле между нажатием и готовым списком стоит с десяток шагов, и половина из них появилась не из красивой архитектуры, а из чеков, на которых предыдущая версия ломалась.

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

    Шаг 1. Что вообще прилетает с телефона

    Принимаем JPEG, PNG, WebP, HEIC и HEIF с айфонов и PDF – если чек пришёл из доставки или почтой. Ограничение – 10 МБ на файл и до десяти фото на один чек: длинную ленту из супермаркета проще снять частями, чем пытаться уместить в кадр целиком.

    Первая же неожиданность – тип файла. Мобильные браузеры регулярно присылают его пустым или сообщают application/octet-stream, особенно при съёмке прямо с камеры. Поэтому тип определяется в три захода: заявленный, потом по расширению имени, а если и его нет – считаем, что это JPEG с камеры. Отказать в загрузке из-за кривого заголовка – худшее, что можно сделать с человеком, который стоит с телефоном над чеком.

    Шаг 2. PDF превращается в картинку

    Модель смотрит на изображение, поэтому PDF растеризуется: первые две страницы рендерятся при 150 dpi и склеиваются вертикально в один JPEG. Двух страниц хватает на любой реальный чек или инвойс.

    Здесь же живёт ограничение, которое выглядит паранойей ровно до первого инцидента: сторона страницы обрезается по 4000 пикселям. Размер страницы объявляет сам PDF, и он может заявить что-нибудь вроде 200×200 дюймов – файла на несколько сотен байт достаточно, чтобы рендер занял несколько гигабайт и выключил сервис целиком. Оверсайз мы не отклоняем, а ужимаем с сохранением пропорций: настоящие чеки этого потолка не достают и рендерятся как были.

    Шаг 3. Предобработка – меньше, чем кажется

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

    Остаётся три вещи: развернуть снимок по EXIF, привести в RGB и перекодировать в JPEG под base64. Разворот – единственное, что реально важно: чек, снятый боком, боком и уедет.

    На этом шаге мы, кстати, споткнулись. Штатная функция поворота в Pillow заодно пересобирает весь блок EXIF – и падает с делением на ноль, если в нём лежит битое рациональное число с нулевым знаменателем. У телефонных камер такое встречается регулярно. Пришлось написать свой разворот: он читает ровно один тег ориентации и не трогает остальное. Всё равно дальше мы кодируем в JPEG без EXIF, так что сохранять больше нечего.

    Шаг 4. Запрос к модели

    Дальше картинка уходит мультимодальной модели с температурой 0 и строгой JSON-схемой: ответ обязан быть валидным объектом с заранее описанными полями, а не текстом, который мы потом разбираем регулярками.

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

    Пока модель думает, сжатая копия фото параллельно уезжает в хранилище – ждать по очереди незачем.

    Шаг 5. Что именно мы просим достать

    Промпт – это, по сути, свод правил чтения чеков, и почти каждая строка в нём выстрадана.

    Позиции – сверху вниз, как напечатано. Если название перенеслось на две строки, склеить в одну. Строку без цены – продолжение или «2 × 150» – отнести к позиции выше. Итог, налоги, скидки и промежуточные суммы в позиции не попадают: это не то, что кто-то ел.

    Заведение – бренд, а не юрлицо. «Пятёрочка», а не ООО «Феникс». В чеке обычно напечатано и то и другое, и без явного указания модель тянет юридическое название – которое человеку ничего не говорит.

    Валюта – код ISO-4217, выведенный из символа, языка, страны, адреса или налоговой метки: НДС значит рубли, TVA и MwSt – евро, 消費税 – иены. Отдельно расписана неоднозначность символов: ¥ с иероглифом 円 – это иены, а с 元 – юани; $ бывает американским, канадским, австралийским, мексиканским; kr – шведские, норвежские и датские кроны. Если уверенности нет, модель обязана вернуть null, а не угадывать: на фронте пустая валюта подставится из прошлого чека компании, и это честнее, чем молча записать чужие деньги рублями.

    Язык – двухбуквенный код позиций по ISO 639-1. Именно он потом включает кнопку перевода.

    Шаг 6. Контрольная сумма – самое интересное

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

    Нужны они как контрольная сумма. В странах, где налог не входит в цену, позиции честно складываются в сумму меньше напечатанного итога – разницу дают как раз налог и обслуживание. Мы сравниваем: если сумма надбавок объясняет разрыв, каждая добавляется отдельной строкой, которую можно поделить наравне с блюдами.

    А вот если разрыв надбавками не объясняется – мы не добавляем ничего. Соблазн «дописать недостающее» здесь огромный и ошибочный: необъяснённый разрыв означает не сбор, а потерянную или неверно прочитанную позицию. Придумать строку на недостающую сумму значит спрятать ошибку распознавания и разделить между людьми выдуманные деньги.

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

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

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

    Шаг 7. Язык и перевод

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

    Распознанный код языка как раз и решает, показывать ли кнопку. Нажали – названия позиций и заведения уходят на перевод, каждая позиция со своим идентификатором, чтобы ответ лёг обратно строка в строку. Оригинальные названия при этом никуда не деваются: в чеке из Барселоны полезно видеть и «Pan con tomate», и то, что это хлеб с помидором.

    Экран Пополамы с распознанным чеком из кафе в Барселоне: 17 позиций на 220,40 евро, слева исходные названия по-испански, справа переведённые на русский
    Валюта определилась с фото, язык – тоже; перевод включился отдельным шагом.

    Шаг 8. Из ответа модели – в чек

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

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

    Что оказалось важнее всего

    Если коротко: качество ИИ-сканера чеков решает не выбор модели. Модели меняются каждые полгода, и подставить новую – это одна строка в цепочке.

    Решает то, что вокруг: не отвалиться на кривом заголовке с телефона, не лечь от стобайтового PDF, не потерять поворот из-за битого EXIF, не угадать валюту наугад и – главное – не дописать в чек сумму, которой там не было.

    Частые вопросы

    Как ИИ распознаёт чек по фото?
    Снимок разворачивают по EXIF, приводят к JPEG и отдают мультимодальной модели вместе с инструкцией и строгой JSON-схемой ответа. Модель читает чек целиком и возвращает структуру: позиции с ценами, валюту в коде ISO-4217 и язык. Классический OCR такого не делает – он возвращает только текст, который потом ещё надо разобрать на поля.
    Как определяется валюта чека?
    По символу валюты, языку чека, стране, адресу и налоговой метке: НДС означает рубли, TVA и MwSt – евро, 消費税 – иены. Неоднозначные символы разводятся по контексту: ¥ рядом с 円 – это иены, рядом с 元 – юани. Если уверенности нет, валюта остаётся пустой и подставляется из прошлого чека компании – угадывать нельзя.
    Как проверяется, что чек распознан правильно?
    Напечатанный на чеке итог и явно подписанные надбавки (налог, сервисный сбор) используются как контрольная сумма. Если сумма позиций расходится с итогом ровно на величину надбавок, они добавляются отдельными строками. Если расхождение надбавками не объясняется, не добавляется ничего: это признак потерянной позиции, а не сбора, и дописывать недостающую сумму нельзя.
    Переводятся ли названия позиций с иностранного чека?
    Да, отдельным шагом по кнопке. Модель возвращает код языка позиций, и если чек не на русском, появляется кнопка перевода. Названия заведения и позиций переводятся на русский, при этом оригинальные названия остаются видны.

    Читайте также