Steven
Steven7 мин. чтения

От OCR к пикселям, не теряя запасной путь

GeekBye перестал читать твои скриншоты через OCR и начал отправлять само изображение зрячим моделям — и это лёгкая часть. Поучительная часть — оптимизация, которая пропускала OCR целиком для пользователей со зрячей моделью, тихо ломала запасной путь к моделям без зрения и её пришлось откатить до версии, где страховочная сетка бежит параллельно и не стоит ничего.

Инженерия
AI
Десктоп
Релизы GeekBye
От OCR к пикселям, не теряя запасной путь

Перед любым приложением, что позволяет сделать скриншот и спросить о нём ИИ, стоит настоящая развилка. Ты можешь запустить OCR — извлечь текст на устройстве, отправить крошечную строку и выбросить картинку. Или ты можешь отправить пиксели — переслать само изображение и дать зрячей модели его прочитать. OCR дёшев и мал, но слеп ко всему, что не текст: к вёрстке, к диаграмме, к отступам кода, к красной волнистой линии под ошибкой. Пиксели сохраняют всё это, но стоят больше байтов и требуют модели, что умеет видеть. GeekBye начинал со стороны OCR этой развилки и, за v1.8.6 и v1.8.7, перешёл на сторону пикселей. Это переключение — лёгкая часть этой истории. Интересная часть — оптимизация, что пришла вместе с ним, сломала запасной путь и её пришлось откатить.

До этого: прочитай текст, брось картинку

До этой главы скриншот становился ответом так: ты жмёшь горячую клавишу захвата, клиент запускает OCR — фреймворк Vision от Apple через свифтовый бинарник VisionOCR на macOS, скрипт PowerShell на Windows, оба за одной абстракцией IOcrEngine — и извлечённый текст заворачивается в строку и отправляется на backend как screenshotText. Само изображение никогда не покидало машину. Это эффективно, и для стены простого текста это нормально. Но скриншот редко бывает просто текстом. Это вакансия с двухколоночной вёрсткой, задача по программированию с диаграммой, ошибка со стек-трейсом, чьи отступы и есть подсказка. OCR сплющивает всё это в построчную транскрипцию, и модель отвечает на более бедный вопрос, чем тот, что ты задал.

Переключение: отправь пиксели (v1.8.6)

v1.8.6 добавил другой путь. Новый imageOptimizer.ts, построенный на sharp и потому кроссплатформенный, делает с сырым скриншотом ровно три вещи:

sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })

Уменьши до максимум 1920px в ширину (никогда не увеличивай меньший захват), преобразуй PNG в JPEG, качество 80. Вывод — сырой base64 с типом медиа, несомым в отдельном поле, и он едет на backend в двух новых полях запроса рядом со старым: screenshotImage и screenshotImageMediaType, независимо от screenshotText. Backend теперь может получить изображение, текст или оба сразу.

Этот шаг оптимизации не косметический. Сырой PNG скриншота — 3–8 MB; уменьшенный JPEG — примерно 200–500 KB. Это на порядок меньше при загрузке, и это к тому же срезает счёт за модель — зрячие модели бьют изображение на тайлы по его разрешению и берут плату за тайл, так что ограничение ширины до 1920 и пережатие напрямую снижают и байты на проводе, и токены изображения, за которые ты платишь, без сколько-нибудь заметной потери читаемости экранного текста.

Откуда приложение вообще знает, умеет ли модель видеть? Об этом объявляет backend. Ответ /api/config получил блок ai.vision с ключом по тарифу, и клиент при каждой загрузке конфигурации кэширует единственное булево значение — aiSupportsVision. Когда зрение поддерживается, оптимизируй изображение; когда нет — откатывайся на OCR. Так что способность — это факт на стороне backend, кэшированный на стороне клиента, а решение для каждого скриншота, запускать OCR или пропустить, принимается на клиенте по этому флагу. Backend по-прежнему владеет тем, какая модель на самом деле отвечает.

Оптимизация, что оказалась слишком умной

Вот здесь становится поучительно. Первая версия зрячего пути запускала OCR и оптимизацию изображения вместе, параллельно. Потом коммит «perf» сжал это в «или-или»: если тариф поддерживает зрение, оптимизируй изображение и пропусти OCR целиком; если нет — запусти OCR и пропусти работу над изображением. На бумаге это очевидный выигрыш — зачем извлекать текст, который зрячей модели не нужен? Changelog даже это рекламирует: «OCR is skipped when the AI can read the image directly.»

Проблема в слове fallback. У backend не одна модель; у него есть основная и запасная, и запасная может быть без зрения. Представь последовательность: основная модель твоего тарифа умеет видеть, так что клиент гордо пропускает OCR и отправляет только изображение. Основной провайдер икает. Backend откатывается на модель, работающую только с текстом, — которой теперь вручают изображение, что она прочитать не может, и никакого текста, потому что клиент оптимизировал текст так, что он исчез. Быстрый путь тихо удалил страховочную сетку. Избыточный быстрый путь можно безопасно отбросить; нельзя отбросить то, в обход чего быстрый путь и был срезкой.

Лекарство: завязывайся на запасной модели, а не на основной

Лекарство — та часть, что стоит стащить. Оно было не в том, чтобы вернуться к всегда запускаемому OCR — это выбросило бы выигрыш для частого случая. Оно было в том, чтобы сделать пропуск зависимым от второго флага способности: умеет ли видеть запасная модель? Конфигурация backend получила блок fallbackVision; клиент кэширует aiFallbackSupportsVision рядом с первым флагом. Теперь у решения четыре ветки:

  • Основная и запасная обе видят → оптимизируй изображение, пропусти OCR. Быстрый, частый путь.
  • Основная видит, но запасная слепа → оптимизируй изображение и запусти OCR, как страховку для запасной.
  • Оптимизация изображения бросает исключение → OCR как жёсткий запасной путь.
  • У тарифа нет зрения вовсе → только OCR.

А вторая ветка — случай со страховкой — это место, где важен последний ход. Запуск OCR после оптимизации изображения добавил бы его задержку сверху. Так что оба бегут вместе под одним Promise.all: оптимизация изображения и OCR перекрываются, и OCR, что существует лишь на случай, если backend его потребует, заканчивается внутри того времени, что изображение и так оптимизировалось. Страховочная сетка бесплатна в терминах настенных часов всякий раз, когда не используется. Весь трюк в одном предложении: сохрани дешёвый запасной путь, завяжи его на то, нужен ли он твоему запасному провайдеру на самом деле, и распараллель его так, чтобы он не стоил ничего, пока тебя не спасёт.

Две меньшие ловушки, что расставил пропуск

У агрессивного пропуска отложенной работы есть привычка вытаскивать на поверхность каждое место, что тихо предполагало: работа случится. Две всплыли в ревью PR на той же неделе.

Взаимная блокировка очереди. Очередь скриншотов считала элемент «готовым», когда у него был текст OCR. Зрячие скриншоты теперь никогда не получали текст OCR — по замыслу — так что проверка полноты hasIncompleteOCR ждала вечно, и очередь зависала. Лекарство переопределяет «готовый» как есть оптимизированное изображение или есть текст OCR, что конвейер теперь и правда гарантирует.

Устаревший флаг способности. aiSupportsVision кэшируется, а значит, может устареть ровно в неверную сторону: пользователь выходит из аккаунта, или загрузка конфигурации проваливается, и клиент остаётся в убеждении, что всё ещё может отправлять изображения модели, которая их больше не поддерживает. Флаг теперь сбрасывается при выходе из аккаунта и при неудачной загрузке конфигурации — у кэшированной способности должен быть сценарий на случай, когда то, что она описывает, исчезает.

Слово о числах

Поскольку смысл этой серии — честность: сообщения коммитов несут цифры — оптимизация изображения «~30ms» против OCR «~100–150ms», нагрузки «~200–500 KB vs ~3–8 MB PNG» — и они правдоподобны, но это собственные встроенные оценки автора, а не вывод бенчмарка. Нигде в истории нет измеренной сквозной задержки до и после. Считай аргумент о скорости здравым проектным замыслом, а не доказанным результатом. Сокращение размера нагрузки — единственное число, которому можно доверять, потому что оно прямо выпадает из уменьшения и пережатия.

Три вещи, которым нас научил этот релиз

  1. Отправка пикселей бьёт чтение текста — когда модель умеет видеть. OCR отбрасывает всё, что скриншот сообщает помимо символов. Зрячая модель читает вёрстку, диаграмму и отступы, а оптимизированный JPEG делает это доступным по цене. Но это ставка на способность, и потому ей нужен запасной путь.
  2. Быстрый путь можно оптимизировать до исчезновения, страховочную сетку — никогда. Пропуск OCR для пользователей со зрячей моделью был верен для основной модели и неверен для запасной. Баг был не в пропуске; он был в том, что пропуск завязали на способность основной модели, когда значение имела способность запасной.
  3. Бесплатная страховочная сетка — это распараллеленная сетка. Завяжи страховочную работу на то, нужна ли она на самом деле, а потом запусти её рядом с быстрым путём, чтобы они перекрывались. Сделанная правильно, плавная деградация не стоит ничего до того дня, когда она — единственная причина, по которой твоя фича всё ещё работает.

Про предыдущую главу истории v1 — два режима отказа накладки, пропускающей клики (v1.8.5); а про всю дугу — анатомию доведения софта до совершенства.

Похожие статьи

Три глагола, что держат Web Audio живым
Steven
Steven8 мин. чтения

Три глагола, что держат Web Audio живым

Два точечных релиза GeekBye, разделённые двумя месяцами и в двух разных файлах, преподали нашему аудиокоду один и тот же урок с противоположных концов: перестань обращаться с браузерным AudioContext как с одноразовым. Один релиз научился делать resume() контексту, который macOS тихо приостановил посреди записи; другой научился suspend() вместо close(), чтобы идущие подряд сессии перестали врезаться в навязанный Chromium потолок примерно в шесть контекстов. Resume, suspend, close — вот и весь сюжет.

Инженерия
Аудио
Десктоп
Отличить звонок от открытого приложения
Steven
Steven8 мин. чтения

Отличить звонок от открытого приложения

GeekBye умеет заметить, что ты присоединился к видеовстрече, и предложить её записать. Обнаружение оказывается лёгкой половиной — нативный Swift-бинарник читает заголовки окон каждые десять секунд. Трудная половина — точность: не срабатывать, когда Zoom просто открыт, не предлагать записать встречу, которую ты уже записываешь, и не заглушать микрофон в звонке, в котором ты на самом деле сидишь. Три релиза, и каждый из них — предохранитель, которому пришлось научиться не побеждать самого себя.

Инженерия
macOS
Десктоп
Убираем backend с пути загрузки
Steven
Steven7 мин. чтения

Убираем backend с пути загрузки

GeekBye записывает твой экран и сохраняет видео в твой Google Drive. Первая версия прогоняла каждую запись через собственные серверы GeekBye по пути туда; релизом позже файл шёл прямо с твоей машины на Drive, а backend был понижен до хранения единственного указателя. Интересная часть в том, как мало кода на самом деле содержит «прямая, возобновляемая» версия — потому что возобновляемость пришла из удаления proxy, а не из его написания.

Инженерия
Архитектура
Десктоп