
От OCR към пиксели, без да загубим fallback-а
GeekBye спря да чете скрийншотовете ти с OCR и започна да изпраща самото изображение към vision моделите — което е лесната част. Поучителната част е оптимизацията, която прескочи OCR изцяло за vision потребителите, тихо счупи fallback-а към не-vision моделите и трябваше да бъде върната до версия, в която предпазната мрежа работи паралелно и не струва нищо.
Съществува истинска развилка на пътя за всяко приложение, което ти позволява да направиш скрийншот на нещо и да питаш 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-а е единственото число, на което можеш да се довериш, защото произтича директно от преоразмеряването и прекомпресирането.
Три неща, на които ни научи това издание
- Изпращането на пикселите бие четенето на текста — когато моделът може да вижда. OCR захвърля всичко, което скрийншотът съобщава, освен знаците. Vision модел чете оформлението, диаграмата и отстъпа, а оптимизиран JPEG прави това достъпно. Но е залог на способност, затова се нуждае от fallback.
- Можеш да оптимизираш до изчезване бърз път, но никога предпазната мрежа. Прескачането на OCR за vision потребителите беше правилно за основния и грешно за fallback-а. Bug-ът не беше прескачането; беше прескачането въз основа на способността на основния, когато тази на fallback-а беше тази, която имаше значение.
- Безплатна предпазна мрежа е паралелизираната. Обвържи застрахователната работа с това дали наистина е нужна, после я пусни редом с бързия път, така че да се припокрива. Направена правилно, плавната деградация не струва нищо до деня, в който е единствената причина функцията ти още да работи.
За предишната глава от историята на v1 — двата режима на отказ на click-through overlay (v1.8.5); а за цялата дъга — анатомията на доставянето на софтуер до съвършенство.