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); а про всю дугу — анатомію доставки програмного забезпечення до досконалості.

Схожі статті

Два режими відмови накладки, що пропускає кліки
Steven
Steven8 хв читання

Два режими відмови накладки, що пропускає кліки

Вікно GeekBye плаває поверх усього й пропускає твої кліки наскрізь — окрім місць, де в нього є кнопки. Це двосторонній контракт, а v1.8.5 і v1.8.14 показують, як виглядає поломка кожної зі сторін: один реліз, де накладка проковтнула системний діалог, і інший, де вона вкрала твої натискання клавіш. Переможним ліком для другого стало видалення коду.

Інженерія
Electron
Десктоп
Релізи в CI, двічі
Steven
Steven7 хв читання

Релізи в CI, двічі

GeekBye v1.8.4 переніс збірки релізів у CI для macOS і Windows. Чого changelog не каже — що ми вже пробували це раніше, видалили місяць потому через вартість ранерів і змусили прижитися лише з другого разу, бо на той момент скрипт релізу вже працював вручну.

Інженерія
CI/CD
Десктоп
Випуск тридцяти мов без страхувальної сітки
Steven
Steven7 хв читання

Випуск тридцяти мов без страхувальної сітки

GeekBye v1.8.3 підключив react-i18next і переклав увесь застосунок на 30 мов. Частина з бібліотекою була рутиною. Цікаве те, чого бракувало: ані перевірки паритету ключів, ані екстрактора рядків — тож зашитий у код англійський текст просочився, у продакшн поїхала данська друкарська помилка, а changelog не міг дійти згоди, скількома мовами говорить застосунок — 28, 29 чи 30.

Інженерія
i18n
Десктоп