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, а не з його написання.

Інженерія
Архітектура
Десктоп