Steven
Steven7 min čtení

Od OCR k pixelům, aniž bychom ztratili fallback

GeekBye přestal číst tvoje screenshoty pomocí OCR a začal posílat samotný obrázek vision modelům — což je ta snadná část. Poučná část je optimalizace, která pro vision uživatele OCR zcela přeskočila, tiše rozbila fallback k non-vision modelům a musela být vrácena k verzi, ve které záchranná síť běží paralelně a nic nestojí.

Inženýrství
AI
Desktop
Vydání GeekBye
Od OCR k pixelům, aniž bychom ztratili fallback

Pro každou aplikaci, která ti umožní udělat screenshot něčeho a zeptat se na to AI, existuje opravdové rozcestí. Můžeš spustit OCR — extrahuješ text na zařízení, pošleš maličký řetězec a zahodíš obrázek. Nebo můžeš poslat pixely — odešleš samotný obrázek a necháš ho přečíst model schopný vision. OCR je levné a malé, ale slepé ke všemu, co není text: k rozvržení, k diagramu, k odsazení kódu, k červené vlnovce pod chybou. Pixely tohle všechno zachovají, ale stojí víc bajtů a vyžadují model, který umí vidět. GeekBye začal na OCR straně toho rozcestí a napříč v1.8.6 a v1.8.7 přešel na stranu pixelů. To přepnutí je ta snadná část příběhu. Zajímavá část je optimalizace, která přišla s ním, rozbila fallback a musela být vrácena zpět.

Předtím: přečti text, zahoď obrázek

Až do tohohle oblouku se screenshot stával odpovědí takhle: stiskneš klávesovou zkratku pro zachycení, klient spustí OCR — Vision framework od Applu přes Swift VisionOCR binárku na macOS, PowerShell skript na Windows, obojí za jednou abstrakcí IOcrEngine — a extrahovaný text se zabalí do řetězce a pošle backendu jako screenshotText. Samotný obrázek nikdy neopustil stroj. To je efektivní a pro zeď prostého textu je to v pořádku. Ale screenshot je zřídka jen text. Je to pracovní inzerát s dvousloupcovým rozvržením, programátorská úloha s diagramem, chyba se stack trace, jehož odsazení je vodítko. OCR tohle všechno zploští do řádek-po-řádku přepisu a model odpoví na tenčí otázku, než jakou jsi položil.

Přepnutí: pošli pixely (v1.8.6)

v1.8.6 přidal druhou cestu. Nový imageOptimizer.ts, postavený na sharp, a proto multiplatformní, dělá se surovým screenshotem přesně tři věci:

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

Změň velikost na nejvýš 1920px šířky (nikdy nezvětšuj menší zachycení), převeď PNG na JPEG, quality 80. Výstup je surový base64 s media typem neseným v samostatném poli a jede na backend ve dvou nových polích požadavku vedle toho starého: screenshotImage a screenshotImageMediaType, nezávisle na screenshotText. Backend teď může přijmout obrázek, text, nebo obojí.

Ten optimalizační krok není kosmetický. Surové screenshot PNG má 3–8 MB; JPEG po změně velikosti má zhruba 200–500 KB. To je o řád méně na uploadu a taky to seřízne účet za model — vision modely dělí obrázek na dlaždice podle jeho rozlišení a účtují za dlaždici, takže omezení šířky na 1920 a znovuzkomprimování přímo snižuje jak bajty na drátě, tak image tokeny, za které platíš, bez smysluplné ztráty čitelnosti textu na obrazovce.

Jak aplikace vůbec ví, jestli model umí vidět? Backend to inzeruje. Odpověď /api/config získala blok ai.vision klíčovaný podle plánu a klient cachuje jediný boolean — aiSupportsVision — při každém načtení configu. Když je vision podporováno, optimalizuj obrázek; když není, spadni zpět na OCR. Takže schopnost je fakt backendu, cachovaný na straně klienta, a rozhodnutí pro každý screenshot, jestli spustit nebo přeskočit OCR, se dělá na klientu z toho flagu. Backend pořád vlastní, který model vlastně odpovídá.

Optimalizace, která byla příliš chytrá

Tady se to stává poučným. První verze vision cesty spouštěla OCR a optimalizaci obrázku spolu, paralelně. Pak jeden „perf" commit to stáhl do buď/anebo: pokud plán podporuje vision, optimalizuj obrázek a přeskoč OCR úplně; pokud ne, spusť OCR a přeskoč práci s obrázkem. Na papíře je to jasná výhra — proč extrahovat text, který vision model nepotřebuje? Changelog to dokonce inzeruje: "OCR is skipped when the AI can read the image directly."

Problém tkví v pojmu fallback. Backend nemá jeden model; má primární a fallback, a fallback může být non-vision model. Představ si tu sekvenci: primární model tvého plánu podporuje vision, takže klient hrdě přeskočí OCR a pošle jen obrázek. Primární poskytovatel škytne. Backend spadne zpět na model jen pro text — kterému se teď předá obrázek, který neumí přečíst, a žádný text, protože klient text zoptimalizoval do zapomnění. Rychlá cesta tiše smazala záchrannou síť. Redundantní rychlou cestu můžeš bezpečně zahodit; nemůžeš zahodit tu věc, kolem které byla rychlá cesta zkratkou.

Oprava: podmiň se fallbackem, ne primárním

Oprava je ta část, kterou stojí za to ukrást. Nebylo to vrátit se k tomu vždycky spouštět OCR — to by zahodilo výhru pro běžný případ. Bylo to udělat přeskočení podmíněné druhým capability flagem: podporuje fallback vision? Config backendu získal blok fallbackVision; klient cachuje aiFallbackSupportsVision vedle prvního flagu. Teď má rozhodnutí čtyři větve:

  • Primární i fallback oba vidí → optimalizuj obrázek, přeskoč OCR. Rychlá, běžná cesta.
  • Primární vidí, ale fallback je slepý → optimalizuj obrázek a spusť OCR, jako pojistku pro fallback.
  • Optimalizace obrázku vyhodí chybu → OCR jako tvrdý fallback.
  • Plán nemá vision vůbec → jen OCR.

A druhá větev — pojistný případ — je místo, kde záleží na posledním tahu. Spustit OCR po optimalizaci obrázku by přidalo jeho latenci zpátky navrch. Takže obojí běží spolu pod jedním Promise.all: optimalizace obrázku a OCR se překrývají a OCR, které existuje jen pro případ, že ho backend bude potřebovat, dokončí v čase, ve kterém se obrázek stejně optimalizoval. Záchranná síť je z hlediska reálného času zdarma pokaždé, když se nepoužije. To je celý trik v jedné větě: ponech si levný fallback, podmiň ho tím, jestli ho tvůj fallback poskytovatel skutečně potřebuje, a paralelizuj ho, aby nic nestál až do dne, kdy tě zachrání.

Dvě menší pasti, které přeskočení spustilo

Agresivní přeskakování odložené práce má sklon vynést na povrch každé místo, které tiše předpokládalo, že se ta práce stane. Dvě vyplula v PR review ve stejném týdnu.

Deadlock ve frontě. Fronta screenshotů považovala položku za „ready," když měla OCR text. Vision screenshoty teď OCR text nikdy nedostaly — záměrně — takže kontrola úplnosti hasIncompleteOCR čekala navždy a fronta se zasekla. Oprava předefinuje „ready" jako má optimalizovaný obrázek nebo má OCR text, což je to, co pipeline teď vlastně zaručuje.

Zastaralý capability flag. aiSupportsVision je cachovaný, což znamená, že může zastarat přesně špatným směrem: uživatel se odhlásí nebo načtení configu selže a klient zůstane v přesvědčení, že pořád může posílat obrázky modelu, který je už nepodporuje. Flag se teď resetuje při odhlášení a při selhání načtení configu — cachovaná schopnost musí mít připravený scénář pro chvíli, kdy to, co popisuje, zmizí.

Slovo o číslech

Protože poctivost je smyslem téhle série: commit zprávy nesou čísla — optimalizace obrázku "~30ms" oproti OCR "~100–150ms," payloady "~200–500 KB vs ~3–8 MB PNG" — a jsou hodnověrná, ale jsou to autorovy vlastní inline odhady, ne výstup benchmarku. Nikde v historii není změřená end-to-end latence před/po. Ber argument rychlosti jako zdravý designový záměr, ne jako prokázaný výsledek. Snížení velikosti payloadu je to jediné číslo, kterému můžeš věřit, protože vypadává rovnou ze změny velikosti a znovuzkomprimování.

Tři věci, které nás tohle vydání naučilo

  1. Posílání pixelů poráží čtení textu — když model umí vidět. OCR zahodí všechno, co screenshot sděluje kromě znaků. Vision model čte rozvržení, diagram a odsazení a optimalizovaný JPEG to dělá dostupným. Ale je to sázka na schopnost, a proto potřebuje fallback.
  2. Rychlou cestu můžeš optimalizovat do zapomnění, záchrannou síť nikdy. Přeskočení OCR pro vision uživatele bylo správné pro primární a špatné pro fallback. Bug nebylo přeskočení; bylo to přeskakování na základě schopnosti primárního, když ta fallbacku byla ta, na které záleželo.
  3. Zadarmo záchranná síť je ta paralelizovaná. Podmiň pojistnou práci tím, jestli je skutečně potřeba, pak ji spusť vedle rychlé cesty, aby se překrývala. Udělaná správně, elegantní degradace nic nestojí až do dne, kdy je jediným důvodem, proč tvoje funkce ještě funguje.

Předchozí kapitolu příběhu v1 najdete v dva režimy selhání click-through overlaye (v1.8.5); a celý oblouk v anatomii dodávání softwaru k dokonalosti.