MacBook M3, 16 гигабайт, никакого облака. Свежая Gemma 4 берёт с картинки график и отдаёт CSV. Первые три кейса — идеально. На четвёртом модель начала врать. И врать аккуратнее, чем говорила правду.
Вводная
Вышла Gemma 4 12B Unified — мультимодальная модель, которая читает не только текст, но и картинки. В квантованном виде она помещается на обычный ноутбук, и мне стало любопытно, что это даёт на практике, а не в бенчмарках.
Просто запустить «hello world» неинтересно. Задача была двойная: собрать на этой модели маленький рабочий инструмент — и заодно честно проверить, где у локального зрения предел. Научился сам — расскажи, как оно на самом деле.
Инструмент выбрал такой, чтобы локальность была оправдана, а не «потому что могу»: вытаскивать данные из картинок с графиками и таблицами в CSV. Это то, что нельзя слить в облако, и то, что сразу грузит vision по полной — OCR, чтение осей, разбор структуры.
Дальше по порядку: что за модель и влезает ли в 16 ГБ, поднимается ли на Mac, на какие грабли я наступил, как устроен инструмент — и карта из семи кейсов, где видно, чему верить, а чему нет.
Зачем локально, если облако читает лучше
Облачные API распознают картинки точнее и быстрее. Но есть данные, которые нельзя выгружать наружу: внутренние дашборды, отчёты под NDA, в общем, визуализация, которую надо оцифровать, не светя в стороннем логе. Тут локальная модель — единственный вариант. Приватность, офлайн, нулевая стоимость инференса. Вопрос один: насколько ей можно верить. Об этом и текст.
Что берём и влезает ли это в 16 ГБ
Герой — Gemma 4 12B Unified. Мультимодальная, encoder‑free: проецирует патчи картинки напрямую, без отдельного визуального энкодера. Контекст до 256K, режим рассуждений гасится одним флагом.
В full precision это ~24 ГБ, в 16 не влезает. Беру квантованную:
-
gemma-4-12b-it-UD-Q4_K_XL.gguf— 6.86 ГБ; -
mmproj-F16.gguf— это «глаза», без него модель текстовая — 167 МБ.
В рантайме mmproj добавляет ~360 МБ. Помещается с запасом, если не выкручивать контекст. Я ставил --ctx-size 8192 — для одной картинки за глаза, а 256K сожрали бы всю память.
Сетап: где я споткнулся
Ставится через Homebrew и llama.cpp. Грабли я собрал на свежести модели — выношу сразу, чтобы ты не повторял.
Качаем модель. Имена файлов идут позиционно, не через --include — иначе CLI скачает только последний:
hf download unsloth/gemma-4-12b-it-GGUF gemma-4-12b-it-UD-Q4_K_XL.gguf mmproj-F16.gguf --local-dir ~/models/gemma-4-12b
Первая засада. Стабильный llama.cpp из Homebrew (билд 9430) при старте с mmproj падал:
clip_init: ... unknown projector type: gemma4uv
gemma4uv — новый визуальный проектор encoder‑free 12B. Поддержку влили в llama.cpp за пару дней до моих экспериментов, и стабильная сборка её ещё не знала. Сборка из исходников через brew install --HEAD упала на компиляции — устаревшие Command Line Tools, «Tier 2 configuration». Сработал готовый официальный бинарник с GitHub Releases, новее билда 9496. Скачал, распаковал, запустил:
~/llama-bin/llama-b9528/llama-server --model ~/models/gemma-4-12b/gemma-4-12b-it-UD-Q4_K_XL.gguf --mmproj ~/models/gemma-4-12b/mmproj-F16.gguf --ctx-size 8192 --jinja --reasoning off --temp 0.1 --top-p 0.95 --top-k 64 --port 8080
Строка в логе, ради которой всё затевалось:
loaded multimodal model, '.../mmproj-F16.gguf'
Зрение поднялось. Урок: на свежих моделях версия раннера решает больше, чем железо. Проверяй номер билда, прежде чем грешить на свой ноут.
Инструмент: 200 строк и один важный промпт
Сам chartscan.py — без внешних зависимостей, только стандартная библиотека. Логика простая: кодируем картинку в base64, шлём в llama-server по OpenAI‑совместимому эндпоинту, парсим строгий JSON, пишем CSV. Весь интеллект — в промпте и схеме:
Возвращай ТОЛЬКО валидный JSON. Схема:{ "kind": "chart" | "table", "chart_type": "bar"|"line"|"pie"|"scatter"|"area"|"other"|null, "title": ..., "columns": [...], "rows": [[...]], "n_labels_seen": целое, // сколько ЯВНЫХ подписей насчитал "value_source": "labeled"|"estimated"|"mixed", "notes": ...}
Четыре решения, которые стоит перенять:
-
temperature=0.1, а не рекомендованная Google1.0. Структурированное извлечение хочет детерминизма, а не разнообразия. Единица — под диалог. -
value_source— модель сама говорит, взяла числа из подписей или прикинула по осям. Звучит как готовое решение проблемы доверия. Спойлер: не работает, вернусь к этому ниже. -
n_labels_seen— модель сперва считает подписи, потом заполняет строки, а скрипт сверяет одно с другим. Разошлось — красный флаг. -
Graceful‑fail. На большой таблице вывод обрывается по лимиту токенов посреди JSON. Скрипт не падает, а дозакрывает скобки и отдаёт все целиком прочитанные строки с пометкой
ВЫВОД ОБРЕЗАН. Недописанную хвостовую строку отбрасывает, а не угадывает.
Честный стресс‑тест: карта попаданий и провалов
Дальше самое интересное. Я взял семь картинок — от тривиальных до намеренно злых — и сверил вывод с оригиналом руками.
Где попадает идеально
Простая таблица (описание полей, 5×4): все ячейки точь‑в-точь, включая пустые (вернула null) и длинный текст с переносами. Сто процентов.

Круговая диаграмма с подписями: все шесть значений (15.2 / 18.2 / 12.1 / 9.1 / 24.2 / 21.2) совпали до десятой. Флаг labeled честный.

Зона комфорта: чистый растр плюс явные числовые подписи. Здесь модели можно верить.
Где честно предупреждает
Stacked bar без подписей. Значения прикинуты по оси, попадание в пределах 1–2 пунктов от реальных границ сегментов, каждая колонка сходится к 100%. И модель сама поставила estimated и объяснила почему. Единственная придирка: выдала 29.5, 20.5 — ложная точность. По сетке с шагом 20 полпроцента не разрешить физически, а десятичные она дорисовала.

Линейный график с 16 плотными подписями — отдельная драма. С первого захода модель потеряла часть точек, переврала значения и выдумала годы 2027–2043, которых на оси нет, достроив её «по плюс четыре года». И при этом нагло пометила всё как labeled. Я ужесточил промпт: запретил достраивать ось (нет подписи — ставь null), потребовал считать подписи, привязал labeled к тому, что и X, и Y взяты из видимого текста. После правки:
-
n_labels_seen: 16, строк 16 — счёт сходится, точки не теряются; -
выдуманные годы исчезли, неподписанные X встали в
null; -
флаг сменился на честный
mixed.
Структурную дыру я закрыл. Но 4 значения из 16 всё равно прочитаны неверно (513→517, 1018→1010, пара 1319/1802 переставлена местами) — всё в загромождённой середине, где подписи налезают на линию. Это уже предел самой модели, промптом не лечится. Зато теперь она об этом честно сигналит.


Где врёт молча — и это страшнее всего
Мыльный скриншот простой зарплатной таблицы. Первая строка прочитана идеально. Вторая, по Петрову, выдумана целиком: 800/1000/800/1000/1200/1400 из оригинала превратились в 1200/1300/1100/1200/1400/1500. Третья — одна ошибка. Подзаголовки аванс/зарплата переврала в важн/план.

И вот тут самое неприятное: рядом стоял флаг labeled: доверяй и счёт сходится. Самоотчёт модели соврал. Кросс‑чек по n_labels_seen считает точки графика, для таблиц он бесполезен — рассинхрон строк не возникает, даже когда половина чисел галлюцинация. Урок: нельзя доверять модели судить о собственной надёжности. Сигнализация должна быть внешней по отношению к ней.
Гигантская вложенная таблица — 30 строк на 18 столбцов, объединённые ячейки, двухуровневая шапка 2019/2020 → квартал → План/Факт. Сначала она вообще не доехала: на ~10 ток/с генерация 4096 токенов тянется минут семь, и клиент отваливался по таймауту раньше, чем сервер договаривал. Поднял таймаут — дождался. Вывод оборвался по лимиту токенов, но graceful‑fail спас 29 строк.


А вот качество спасённого — финал всей истории. Модель прочитала первую ячейку‑две в каждой строке верно, а дальше перестала читать и насыпала гладкую арифметическую прогрессию круглыми тысячами — 10000, 11000, 12000, 13000, — хотя в оригинале рваные 4302, 657, 9892. Объединённые ячейки развалились: товары Nestea уехали под BonAqua. Колонки местами сдвинулись. Появились несуществующие позиции — «братиш», «черника». Последняя строка продублировала предыдущую слово в слово.
Попробуй на глаз отличить это от настоящих данных. В том и подвох: фабрикация выглядит чище правды. Ровные тысячи, аккуратная структура — а это почти весь синтетик.
Главный вывод
Наивная гипотеза «локальная модель честна о своей точности» — неверна. Модель врёт и про числа, и про собственный флаг доверия. Точная формулировка такая:
Локальный VLM полезен только внутри узкой проверяемой зоны — чистые таблицы и простые графики с подписями. За её пределами он не падает, а правдоподобно врёт. И чем аккуратнее выглядит результат, тем больше повод насторожиться: честный OCR рваных реальных данных выглядит грязнее, чем выдуманная гладкая прогрессия.
Отсюда и роль инженера. Она не в том, чтобы выжать последние проценты точности — та упирается в саму модель. Она в том, чтобы натянуть проволочки‑сигнализации: строгая схема, флаг происхождения значений, кросс-чек подписей, graceful‑fail с явной пометкой обрыва. Они не делают модель умнее. Они говорят, когда ей не верить.
Когда брать локально, а когда не мучиться
Бери локально, если: разовая оцифровка чувствительных таблиц и простых графиков с подписями, нужен офлайн, нулевая стоимость, приватность. С обязательной ручной сверкой на сомнительных кейсах.
Не мучайся, если: плотные многосерийные графики, мыльные сканы, огромные вложенные таблицы. Тут либо облачный API, либо человек с глазами.
Вот как выглядит локальный инференс на M3 16 ГБ:
Простой график отдаётся за полминуты, гигантская таблица — за минуты. Практическим потолком на больших задачах оказывается wall‑clock, а не точность.
Локальная 12B на ноуте — это про приватность и контроль, а не про идеальную цифру. Если держать это в голове и не верить гладкому выводу на слово — инструмент рабочий.
Ссылки
-
GGUF‑кванты, которые качал я: https://huggingface.co/unsloth/gemma-4-12b‑it‑GGUF
-
Официальная карточка Gemma 4 12B (instruction‑tuned): https://huggingface.co/google/gemma-4-12B‑it
-
Релизы llama.cpp (бери билд ≥ 9496 ради
gemma4uv): https://github.com/ggml‑org/llama.cpp/releases -
Код
chartscan.py: [https://gist.github.com/inforobotvit/0c90319f61ca20899011265c40c3f60b]
Будешь повторять — проверь номер билда llama.cpp, на свежих моделях это решает.
Если нужен гайд по установке и работе с этой моделью на твоём маке, напиши комментарий — подготовлю и пришлю.
Пишу про практики работы с AI в Telegram‑канале «Я и мой друг робот» — про мульти‑агентные системы, визуализацию данных и реальные автоматизации: https://t.me/mewithrobot
Автор: VitTurov


