Steven
Steven7 мин четене

От OCR към пиксели, без да загубим fallback-а

GeekBye спря да чете скрийншотовете ти с OCR и започна да изпраща самото изображение към vision моделите — което е лесната част. Поучителната част е оптимизацията, която прескочи OCR изцяло за vision потребителите, тихо счупи fallback-а към не-vision моделите и трябваше да бъде върната до версия, в която предпазната мрежа работи паралелно и не струва нищо.

Инженерство
AI
Desktop
Издания на GeekBye
От OCR към пиксели, без да загубим fallback-а

Съществува истинска развилка на пътя за всяко приложение, което ти позволява да направиш скрийншот на нещо и да питаш AI за него. Можеш да пуснеш OCR — извличаш текста на устройството, изпращаш мъничък низ и изхвърляш картината. Или можеш да изпратиш пикселите — доставяш самото изображение и оставяш способен на vision модел да го прочете. OCR е евтин и малък, но сляп за всичко, което не е текст: оформлението, диаграмата, отстъпа на кода, червената вълничка под грешката. Пикселите запазват всичко това, но струват повече байтове и изискват модел, който може да вижда. GeekBye започна от OCR страната на тази развилка и през v1.8.6 и v1.8.7 премина към страната на пикселите. Онова превключване е лесната част от тази история. Интересната част е оптимизацията, която дойде с него, счупи fallback-а и трябваше да бъде отменена.

Преди: чети текста, изхвърли картината

Досега в тази дъга скрийншотът ставаше отговор така: натискаш клавишната комбинация за заснемане, клиентът пуска OCR — Vision framework-ът на Apple през Swift VisionOCR binary на macOS, PowerShell скрипт на Windows, и двете зад една абстракция IOcrEngine — и извлеченият текст се опакова в низ и се изпраща към backend-а като screenshotText. Самото изображение никога не напускаше машината. Това е ефективно и за стена от обикновен текст е добре. Но скрийншотът рядко е само текст. Той е обява за работа с оформление в две колони, задача по програмиране с диаграма, грешка със stack trace, чийто отстъп е ключът. OCR сплесква всичко това в ред-по-ред транскрипция и моделът отговаря на по-тънък въпрос от този, който зададе.

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

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

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

Преоразмерява до най-много 1920px ширина (никога не уголемявай по-малко заснемане), конвертира PNG в JPEG, quality 80. Изходът е суров base64 с media type, носен в отделно поле, и пътува към backend-а в две нови полета на заявката наред със старото: screenshotImage и screenshotImageMediaType, независимо от screenshotText. Backend-ът вече може да получи изображение, текст или и двете.

Тази стъпка на оптимизация не е козметична. Суров PNG скрийншот е 3–8 MB; преоразмереният JPEG е приблизително 200–500 KB. Това е с порядък по-малко за качването и също така реже сметката на модела — vision моделите разбиват изображение на плочки според резолюцията му и таксуват на плочка, така че ограничаването на ширината до 1920 и прекомпресирането директно намаляват както байтовете по мрежата, така и image tokens, за които плащаш, без значима загуба на четимост за текста на екрана.

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

Оптимизацията, която беше прекалено хитра

Ето къде става поучително. Първият вариант на vision пътя пускаше OCR и оптимизацията на изображението заедно, паралелно. После един „perf" commit го стегна в едно или/или: ако планът поддържа vision, оптимизирай изображението и прескочи OCR изцяло; ако не, пусни OCR и прескочи работата с изображението. На хартия това е очевидната победа — защо да извличаш текст, от който vision моделът не се нуждае? Changelog-ът дори го рекламира: "OCR is skipped when the AI can read the image directly."

Проблемът се крие в понятието fallback. Backend-ът няма един модел; има основен и fallback, а fallback-ът може да е не-vision модел. Представи си последователността: основният модел на плана ти поддържа vision, така че клиентът гордо прескача OCR и изпраща само изображението. Основният доставчик засича. Backend-ът пада към модел само за текст — на който сега се подава изображение, което не може да прочете, и никакъв текст, защото клиентът е оптимизирал текста до изчезване. Бързият път тихо беше изтрил предпазната мрежа. Можеш безопасно да изхвърлиш излишен бърз път; не можеш да изхвърлиш нещото, около което бързият път беше пряка пътека.

Fix-ът: обвържи се с fallback-а, не с основния

Fix-ът е частта, която си струва да откраднеш. Не беше връщане към това винаги да се пуска OCR — това би захвърлило победата за честия случай. Беше да се направи прескачането условно спрямо втори capability флаг: fallback-ът поддържа ли vision? Config-ът на backend-а получи блок fallbackVision; клиентът кешира aiFallbackSupportsVision до първия флаг. Сега решението има четири разклонения:

  • Основният и fallback-ът виждат и двата → оптимизирай изображението, прескочи OCR. Бързият, чест път.
  • Основният вижда, но fallback-ът е сляп → оптимизирай изображението и пусни OCR, като застраховка за fallback-а.
  • Оптимизацията на изображението хвърля грешка → OCR като твърд fallback.
  • Планът изобщо няма vision → само OCR.

А второто разклонение — застрахователният случай — е където има значение последният ход. Пускането на OCR след оптимизацията на изображението би добавило латентността му обратно отгоре. Затова двете се пускат заедно под един Promise.all: оптимизацията на изображението и OCR се припокриват, а OCR-ът, който съществува само в случай че backend-ът се нуждае от него, завършва в рамките на времето, през което изображението и без това се оптимизираше. Предпазната мрежа е безплатна откъм реално време винаги когато не се използва. Това е целият трик в едно изречение: запази евтиния fallback, обвържи го с това дали fallback доставчикът ти наистина се нуждае от него, и го паралелизирай, така че да не струва нищо до деня, в който те спасява.

Два по-малки капана, които прескачането задейства

Агресивното прескачане на отложена работа има навика да изважда наяве всяко място, което тихо е приемало, че работата ще се случи. Две изплуваха в PR review-то същата седмица.

Deadlock в опашката. Опашката със скрийншоти смяташе елемент за „ready," когато имаше OCR текст. Vision скрийншотовете сега никога не получаваха OCR текст — по замисъл — така че проверката за пълнота hasIncompleteOCR чакаше вечно и опашката увисваше. Fix-ът предефинира „ready" като има оптимизирано изображение или има OCR текст, което е това, което pipeline-ът вече всъщност гарантира.

Остарял capability флаг. aiSupportsVision е кеширан, което означава, че може да остарее точно в грешната посока: потребител излиза от профила си или извличане на config се проваля, и клиентът остава да вярва, че все още може да изпраща изображения към модел, който вече не ги поддържа. Флагът сега се нулира при изход от профила и при провал на извличането на config — кеширана способност трябва да има план за момента, в който нещото, което описва, изчезне.

Няколко думи за числата

Защото честността е смисълът на тази поредица: commit съобщенията носят цифри — оптимизация на изображението "~30ms" срещу OCR "~100–150ms," payload-и "~200–500 KB vs ~3–8 MB PNG" — и те са правдоподобни, но са собствените inline оценки на автора, а не резултат от benchmark. Никъде в историята няма измерена end-to-end латентност преди/след. Приемай аргумента за скоростта като здрав дизайнерски замисъл, не като доказан резултат. Намаляването на размера на payload-а е единственото число, на което можеш да се довериш, защото произтича директно от преоразмеряването и прекомпресирането.

Три неща, на които ни научи това издание

  1. Изпращането на пикселите бие четенето на текста — когато моделът може да вижда. OCR захвърля всичко, което скрийншотът съобщава, освен знаците. Vision модел чете оформлението, диаграмата и отстъпа, а оптимизиран JPEG прави това достъпно. Но е залог на способност, затова се нуждае от fallback.
  2. Можеш да оптимизираш до изчезване бърз път, но никога предпазната мрежа. Прескачането на OCR за vision потребителите беше правилно за основния и грешно за fallback-а. Bug-ът не беше прескачането; беше прескачането въз основа на способността на основния, когато тази на fallback-а беше тази, която имаше значение.
  3. Безплатна предпазна мрежа е паралелизираната. Обвържи застрахователната работа с това дали наистина е нужна, после я пусни редом с бързия път, така че да се припокрива. Направена правилно, плавната деградация не струва нищо до деня, в който е единствената причина функцията ти още да работи.

За предишната глава от историята на v1 — двата режима на отказ на click-through overlay (v1.8.5); а за цялата дъга — анатомията на доставянето на софтуер до съвършенство.

Свързани статии

Три глагола, които поддържат Web Audio жив
Steven
Steven9 мин четене

Три глагола, които поддържат Web Audio жив

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

Инженерство
Audio
Desktop
Да различиш обаждане от отворено приложение
Steven
Steven8 мин четене

Да различиш обаждане от отворено приложение

GeekBye може да забележи, че си се присъединил към видео среща, и да ти предложи да я запише. Разпознаването се оказва лесната половина — Swift binary, който чете заглавия на прозорци на всеки десет секунди. Трудната половина е прецизността: да не се задейства, когато Zoom е просто отворен, да не пита за среща, която вече записваш, и да не заглушава микрофона в обаждането, в което всъщност си. Три издания, всяко от които е предпазител, който трябваше да се научи да не побеждава сам себе си.

Инженерство
macOS
Desktop
Изваждане на backend-а от пътя на качването
Steven
Steven8 мин четене

Изваждане на backend-а от пътя на качването

GeekBye записва екрана ти и запазва видеото в твоя Google Drive. Първата версия прекарваше всеки запис през собствените сървъри на GeekBye по пътя натам; едно издание по-късно, файлът тръгваше директно от машината ти към Drive, а backend-ът беше понижен до това да държи един-единствен указател. Интересната част е колко малко код всъщност съдържа „директната, resumable" версия — защото resumability дойде от изтриването на един proxy, не от написването на такъв.

Инженерство
Архитектура
Desktop