Разберём, как распознать текст с картинки через Ollama: отправим base64-картинку нативным запросом к локальному /api/chat и сверим ответ построчно и по числам с заранее выписанным эталоном. Ниже — полный код запроса, два синтетических чека и независимый чекер.
Что понадобится и как устроена проверка
Задача — получить дословный текст PNG напрямую через Ollama, без n8n, LangChain и community web-обёртки. Используем vision-модель deepseek-ocr:3b. Её карточка требует Ollama v0.13.0+ и предупреждает о чувствительности к формулировке запроса; простой OCR-префикс ниже сохранён с атрибуцией. Проверка не измеряет качество всех моделей и не является прогоном редакции.
Вход — синтетический учебный чек ocr_fixture_a.png (1000×360, чёрные символы на белом). Ниже приведён его полный эталон, а затем генератор обоих PNG и файлов expected. Генератор выполняется обычным Python и не обращается к модели. Полный эталон A:
TRAINING RECEIPT R-027
Date: 2026-11-05
Notebooks 3 x 240 = 720 RUB
Delivery 90 RUB
Total 810 RUB
Арифметика исходника: 3×240+90=810. Второй PNG отличается строками Delivery 120 RUB и Total 840 RUB. Создайте оба входа и эталоны следующим генератором make_fixture.py. Понадобится библиотека Pillow (python3 -m pip install Pillow) и путь к существующему TTF-шрифту, например DejaVuSansMono.
from pathlib import Path
import sys
from PIL import Image, ImageDraw, ImageFont
folder = Path(__file__).resolve().parent
font = ImageFont.truetype(sys.argv[1], 28)
for suffix, delivery in [("a", 90), ("b", 120)]:
lines = [
"TRAINING RECEIPT R-027",
"Date: 2026-11-05",
"Notebooks 3 x 240 = 720 RUB",
f"Delivery {delivery} RUB",
f"Total {720 + delivery} RUB",
]
image = Image.new("RGB", (1000, 360), "white")
draw = ImageDraw.Draw(image)
for i, line in enumerate(lines):
draw.text((36, 32 + i * 57), line, font=font, fill="black")
image.save(folder / f"ocr_fixture_{suffix}.png")
(folder / f"expected_{suffix}.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
Запустите python3 make_fixture.py /полный/путь/к/шрифту.ttf, подставив свой путь. Рядом со скриптом появятся оба PNG и файлы expected_a.txt/expected_b.txt. Откройте PNG и визуально сравните все пять строк с эталонами перед запросом к модели.
На компьютере читателя должен работать Ollama с моделью deepseek-ocr:3b; установить её можно командой ollama pull deepseek-ocr:3b. Доступность памяти проверяйте для своего оборудования. Сохраните скрипт run_ocr.py рядом с изображениями: он читает PNG, кодирует его в base64 и обращается к локальному /api/chat.
Документация Ollama по Vision описывает, что vision-модели принимают изображения вместе с текстом, а REST API ожидает base64-кодированные данные изображения. Документация по /api/chat описывает поле messages как массив объектов сообщений и параметр stream со значением по умолчанию true; в нашем скрипте stream выставлен в false, чтобы получить один JSON-ответ.
| Что проверяем | Инструмент | Ожидаемый итог |
|---|---|---|
| Текст из PNG | run_ocr.py → /api/chat |
JSON с полем message.content |
| Совпадение строк | check_ocr.py |
all_lines_exact: PASS |
| Совпадение чисел | check_ocr.py |
numbers_in_order_exact: PASS |
| Код завершения | check_ocr.py |
0 |
Полный код нативного запроса
Следующий полный скрипт run_ocr.py читает PNG, кодирует его в base64 и обращается к локальному /api/chat. Такой images-массив соответствует текущей документации Vision; stream:false нужен для одного JSON-ответа. Запрос Extract the text in the image. взят из официальной карточки OCR-модели, а сам вход и проверяющий код ниже — самостоятельный артефакт.
"""User-run native request; editorial author did not execute this request."""
import base64,json,sys
from pathlib import Path
from urllib.request import Request,urlopen
image=Path(sys.argv[1])
payload={'model':'deepseek-ocr:3b','messages':[{'role':'user','content':'Extract the text in the image.','images':[base64.b64encode(image.read_bytes()).decode('ascii')]}],'stream':False}
request=Request('http://localhost:11434/api/chat',data=json.dumps(payload).encode(),headers={'Content-Type':'application/json'})
with urlopen(request,timeout=300)as response:
data=json.load(response)
print(data['message']['content'])
Пользовательский запуск:
python3 run_ocr.py ocr_fixture_a.png > actual_a.txt
python3 run_ocr.py ocr_fixture_b.png > actual_b.txt
Это команды для будущего самостоятельного запуска. actual_a.txt и actual_b.txt в редакционном пакете отсутствуют: фактических ответов модели не было. Отсутствующие actual-файлы не заменены выдуманным ответом модели — их создаёт сам читатель своим запуском.
Разбор частой ошибки: адрес http://localhost:11434/api/chat совпадает с примером из документации Ollama по Chat endpoint. Если Ollama слушает другой порт или хост, запрос вернёт ошибку соединения — проверьте адрес в request=Request(...) до запуска. В нашем скрипте заголовок Content-Type: application/json явно указывает формат тела запроса.
Независимая сверка с эталоном
Полный проверяющий скрипт check_ocr.py:
"""Compare user OCR text with explicit synthetic ground truth."""
from pathlib import Path
import re,sys
expected=Path(sys.argv[1]).read_text(encoding='utf-8')
actual=Path(sys.argv[2]).read_text(encoding='utf-8')
def lines(text):return [line.strip()for line in text.splitlines()if line.strip()]
def numbers(text):return re.findall(r'\d+(?:[.,]\d+)?',text)
checks={'all_lines_exact':lines(actual)==lines(expected),'numbers_in_order_exact':numbers(actual)==numbers(expected)}
for name,passed in checks.items():print(name+': '+('PASS'if passed else'FAIL'))
if not all(checks.values()):raise SystemExit(1)
Проверки:
python3 check_ocr.py expected_a.txt actual_a.txt
python3 check_ocr.py expected_b.txt actual_b.txt
Эталон успешной сверки каждого файла — две строки all_lines_exact: PASS и numbers_in_order_exact: PASS, код завершения 0. Эти строки описывают ожидаемый отчёт при успешной сверке. Фактический ответ модели читатель получает своим запуском. Сверка удаляет только пустые строки и пробелы по краям; знаки, цифры и остальные символы остаются значимыми. Поэтому изменение форматирования может дать строгий FAIL: разбирайте разницу.
Первая конкретная проверка: в A должны остаться все пять строк, номер R-027, дата и сумма 810 без добавленных реквизитов. Вторая: B обязан вернуть доставку 120 и итог 840; старый итог 810 отвергается. Если текст неполный или придуманы числа, результат не принимается.
| Файл | Ожидаемые строки | Ключевые числа | Итог |
|---|---|---|---|
| A | 5 строк, номер R-027, дата 2026-11-05 | 3, 240, 720, 90, 810 | 810 RUB |
| B | те же строки, изменены две | 3, 240, 720, 120, 840 | 840 RUB |
Два входа: доставка и итог
Два PNG различаются доставкой и итогом. Если ответ для B содержит старый итог 810, результат расходится с эталоном и отклоняется; причину ошибки нужно разбирать отдельно. Чекер сравнивает все числа в порядке появления, включая номер и дату: для B это 027, 2026, 11, 05, 3, 240, 720, 120, 840. Возврат старых значений доставки или итога даст numbers_in_order_exact: FAIL.
Практический порядок действий:
- Установите модель:
ollama pull deepseek-ocr:3b. - Положите
run_ocr.py,check_ocr.py, оба PNG и обаexpected_*.txtв одну папку. - Запустите
run_ocr.pyдля A и сохраните вывод вactual_a.txt. - Запустите
run_ocr.pyдля B и сохраните вывод вactual_b.txt. - Прогоните
check_ocr.pyдля каждой пары и посмотрите на две строки PASS/FAIL. - При FAIL откройте diff между
expectedиactual: сначала ищите пропущенную строку, затем лишние символы, затем подменённое число.
Доставка и итог связаны арифметикой: 3×240+90=810 в A и 3×240+120=840 в B. Эта связь полезна тем, что подмена одного числа ломает сразу и строку, и итог. Проверка по числам в порядке появления ловит такую подмену без разбора всего текста.
Границы примера и альтернативная модель
Успех на этих двух простых картинках не доказывает пригодность для сканов, кириллицы или таблиц: для своей задачи добавьте собственные изображения и заранее выписанный эталон. Один простой пример не объявляется бенчмарком точности. Сравнение строк здесь даёт более строгую опору, чем просьба второй модели угадать, верно ли распознан документ.
Если нужен другой инструмент, в реестре Ollama есть карточка glm-ocr — мультимодальной OCR-модели для понимания сложных документов на архитектуре GLM-V encoder–decoder. В карточке указаны команды вида ollama run glm-ocr Text Recognition: ./image.png, размеры вариантов 2.7GB, 1.8GB и 2.7GB, контекст 128K и заявленный результат 94.62 на OmniDocBench V1.5. Это чужие заявления владельца карточки, они не переносятся на наш пример и не подтверждают качество на ваших файлах.
| Критерий | deepseek-ocr:3b |
glm-ocr |
|---|---|---|
| Запуск | ollama pull deepseek-ocr:3b |
ollama run glm-ocr |
| Вход | PNG в base64 через /api/chat |
PNG в команде карточки |
| Проверка | свой check_ocr.py |
свой эталон |
| Заявленный бенчмарк | нет в нашем пакете | 94.62 на OmniDocBench V1.5 |
Выбор модели не отменяет главного шага: эталон выписывается до запуска, а результат сверяется построчно и по числам. Модель может вернуть правдоподобный текст с подменённой цифрой, и без эталона такую подмену заметить сложно.
Частые вопросы
Как запустить Ollama OCR для PNG без облачных сервисов?
Установите модель командой ollama pull deepseek-ocr:3b, сохраните run_ocr.py рядом с картинкой и запустите python3 run_ocr.py ocr_fixture_a.png > actual_a.txt. Скрипт кодирует PNG в base64 и отправляет его на локальный http://localhost:11434/api/chat с stream:false. Ответ приходит в поле message.content одного JSON-объекта. Облачная цепочка в этой схеме не участвует.
Почему проверка даёт FAIL, хотя текст распознан верно?
Чекер удаляет только пустые строки и пробелы по краям, остальные символы значимы. Лишний пробел внутри строки, изменённый регистр или другой разделитель дают строгий FAIL. Откройте diff между expected и actual и разберите конкретное расхождение. Автоматически называть такое расхождение галлюцинацией не стоит.
Чем отличаются два тестовых PNG?
ocr_fixture_a.png содержит доставку 90 RUB и итог 810 RUB, ocr_fixture_b.png — доставку 120 RUB и итог 840 RUB. Остальные строки совпадают: номер R-027, дата 2026-11-05, позиция Notebooks 3 x 240 = 720 RUB. Эти изменения помогают обнаружить возврат старых чисел, но сами по себе не устанавливают причину ошибки.
Можно ли использовать этот пример как бенчмарк точности?
Нет. Два синтетических чека с ASCII-текстом не покрывают сканы, кириллицу, таблицы и печати. Для своей задачи добавьте собственные изображения и выпишите эталон заранее. Заявленные чужие результаты на OmniDocBench относятся к карточке glm-ocr и не переносятся на ваш случай.
Что делать, если модель добавляет лишние реквизиты?
Дописанные строки, которых нет в эталоне, ломают all_lines_exact, и проверка завершается кодом 1. Сравните списки строк и уберите из промпта всё, что провоцирует домысливание. Формулировка запроса влияет на результат, поэтому фиксируйте её и меняйте по одному элементу за раз.
